#!/usr/bin/env node
/**
 * Merge HubSpot blog-export CSV metadata into existing CMS pages (by slug): `name`,
 * `previewDescription`, and `metaTags` (SEO + OG fields), without touching linked HTML.
 *
 * Defaults to the same slugs as `scripts/fix-blog-publish-dates.js` (single source:
 * scripts/lib/blog-metadata-target-rows.js). Optional `--all-csv-rows` updates every CSV
 * row whose slug exists in CMS.
 *
 * Run:
 *   BILD_APP_ID=<app-id> node scripts/upsert-blog-metadata-from-hubspot-csv.js --dry-run [/path/export.csv]
 *   node scripts/upsert-blog-metadata-from-hubspot-csv.js bildit-production-website [/path/export.csv]
 *
 * Environment:
 *   HUBSPOT_BLOG_CSV — default CSV path if not passed as argv (else ~/Downloads/hubspot-blog-export-empty-2026-05-07.csv)
 *
 * Optional flags:
 *   --dry-run
 *   --publish-from-csv      — sets `published` + `schedule` from CSV `published_at`
 *   --also-created-at       — sets `createdAt` from TARGET_BLOG_ROWS (same as fix-blog-publish-dates)
 *   --all-csv-rows          — process each CSV slug that resolves in CMS (not only TARGET_BLOG_ROWS)
 */

'use strict'

const fs = require('fs')
const path = require('path')

const { getPage, updatePage } = require('../src/lib/api')
const {
  resolvePageIdByLookup,
  pagePutPayloadFromExisting,
  normalizeMetaTagsForPut,
  mergePageMetaTags,
  coerceAuditTimestampToUnixMs,
} = require('../src/lib/pages-cli')
const {
  parseBlogCsvFile,
  metaTagsFromCsvRow,
  normalizeContentSlug,
  resolveImagePathRelativeToCsv,
  mergePageScheduleWithPublishedAt,
  parsePublishedAtMs,
} = require('../src/lib/blog-csv')
const { TARGET_BLOG_ROWS } = require('./lib/blog-metadata-target-rows')

const DEFAULT_HUBSPOT_DOWNLOAD = path.join(
  path.resolve(require('os').homedir(), 'Downloads'),
  'hubspot-blog-export-empty-2026-05-07.csv'
)

/** @typedef {{ slug: string; slugNorm: string; row: Record<string, string>; createdAtIso?: string; note?: string }} WorkItem */

function parseFlags(argv) {
  return {
    dryRun: argv.includes('--dry-run'),
    publishFromCsv: argv.includes('--publish-from-csv'),
    alsoCreatedAt: argv.includes('--also-created-at'),
    allCsvRows: argv.includes('--all-csv-rows'),
  }
}

/**
 * @param {string[]} argv
 * @returns {{ appId?: string; csvPathAbs?: string; flags: ReturnType<typeof parseFlags> }}
 */
function resolveCli(argv) {
  const flags = parseFlags(argv)
  const positional = argv.slice(2).filter((a) => !String(a).startsWith('--'))

  /** @type {string|undefined} */
  let csvPathAbs
  /** @type {string|undefined} */
  let appId

  for (const p of positional) {
    const s = String(p).trim()
    if (!s) continue
    if (/\.csv$/i.test(s) || s.includes('/') || s.includes('\\')) {
      csvPathAbs = path.resolve(s)
    } else if (!appId) {
      appId = s
    }
  }

  if (!csvPathAbs) {
    const env = process.env.HUBSPOT_BLOG_CSV && String(process.env.HUBSPOT_BLOG_CSV).trim()
    if (env) {
      const r = path.resolve(env)
      if (fs.existsSync(r)) csvPathAbs = r
    }
    if (!csvPathAbs && fs.existsSync(DEFAULT_HUBSPOT_DOWNLOAD)) {
      csvPathAbs = DEFAULT_HUBSPOT_DOWNLOAD
    }
  }

  if (!appId) appId = (process.env.BILD_APP_ID && String(process.env.BILD_APP_ID).trim()) || undefined

  return { appId, csvPathAbs, flags }
}

/**
 * @param {Record<string, string>} row
 * @param {string} csvAbs
 */
function summarizeMetaKeys(row, csvAbs) {
  const resolvedOg = resolveImagePathRelativeToCsv(csvAbs, row.image ?? '')
  const mt =
    metaTagsFromCsvRow(row, {
      resolvedOgImageUrl: resolvedOg,
    }) ?? {}
  return Object.keys(mt).sort().join(',')
}

/**
 * @param {Map<string, Record<string, string>>} rowsBySlug
 * @param {ReturnType<typeof parseFlags>} flags
 * @returns {WorkItem[]}
 */
function buildWorklist(rowsBySlug, flags) {
  /** @type {Map<string, string>} */
  const createdAtBySlug = new Map(TARGET_BLOG_ROWS.map((r) => [normalizeContentSlug(r.slug), r.createdAtIso]))

  if (flags.allCsvRows) {
    /** @type {WorkItem[]} */
    const out = []
    for (const row of rowsBySlug.values()) {
      const slugNorm = normalizeContentSlug(row.slug ?? '')
      if (!slugNorm) continue
      out.push({
        slug: String(row.slug ?? '').trim(),
        slugNorm,
        row,
        createdAtIso: createdAtBySlug.get(slugNorm),
      })
    }
    out.sort((a, b) => a.slugNorm.localeCompare(b.slugNorm))
    return out
  }

  /** @type {WorkItem[]} */
  const ordered = []
  for (const t of TARGET_BLOG_ROWS) {
    const slugNorm = normalizeContentSlug(t.slug)
    const row = rowsBySlug.get(slugNorm)
    if (!row) {
      console.error(`warn no CSV row for slug="${t.slug}" — skipped`)
      continue
    }
    ordered.push({
      slug: t.slug,
      slugNorm,
      row,
      createdAtIso: t.createdAtIso,
      note: t.note,
    })
  }
  return ordered
}

/**
 * @param {string} appId
 * @param {WorkItem} item
 * @param {{ dryRun: boolean; publishFromCsv: boolean; alsoCreatedAt: boolean }} options
 * @param {string} csvAbs
 */
async function upsertOne(appId, item, options, csvAbs) {
  const { dryRun, publishFromCsv, alsoCreatedAt } = options
  const slugForLookup = String(item.row.slug ?? item.slugNorm ?? '').trim() || item.slug

  const createdIso =
    item.createdAtIso && String(item.createdAtIso).trim() ? String(item.createdAtIso).trim() : undefined

  if (dryRun) {
    const title = String(item.row.title ?? '').trim()
    const prev = String(item.row.excerpt ?? item.row.meta_description ?? '').trim().slice(0, 72)
    const metaKeys = summarizeMetaKeys(item.row, csvAbs)
    const parts = [`name=${JSON.stringify(title)}`, `preview=${JSON.stringify(prev)}`, `metaTags keys: ${metaKeys || '(none)'}`]
    if (publishFromCsv && String(item.row.published_at ?? '').trim()) parts.push(`schedule← published_at`)
    if (alsoCreatedAt && createdIso) parts.push(`createdAt← ${createdIso}`)
    console.log(`[dry-run] slug:${slugForLookup} ${parts.join(' | ')}`)
    return { ok: true, key: `slug:${slugForLookup}` }
  }

  const pid = await resolvePageIdByLookup({ appId, slug: slugForLookup })
  const existing = await getPage(appId, pid)
  if (!existing || typeof existing !== 'object') throw new Error('Could not load page from CMS')

  /** @type {Record<string, unknown>} */
  const body = pagePutPayloadFromExisting(existing)

  const name = String(item.row.title ?? '').trim()
  if (name) body.name = name

  const excerpt = String(item.row.excerpt ?? '').trim()
  const metaDesc = String(item.row.meta_description ?? '').trim()
  const previewCsv = excerpt || metaDesc
  if (previewCsv) body.previewDescription = previewCsv

  const resolvedOg = resolveImagePathRelativeToCsv(csvAbs, item.row.image ?? '')
  const csvMetaPartial = metaTagsFromCsvRow(item.row, { resolvedOgImageUrl: resolvedOg })
  const mergedMeta = mergePageMetaTags(normalizeMetaTagsForPut(existing), csvMetaPartial)
  if (mergedMeta && typeof mergedMeta === 'object') body.metaTags = mergedMeta

  if (publishFromCsv) {
    const rawPub = String(item.row.published_at ?? '').trim()
    if (rawPub) {
      const ts = Date.parse(rawPub)
      if (!Number.isNaN(ts)) {
        body.published = true
        body.schedule = mergePageScheduleWithPublishedAt(existing.schedule, rawPub)
      }
    }
  }

  if (alsoCreatedAt && createdIso) {
    const updatedMs = coerceAuditTimestampToUnixMs(existing, 'updatedAt', 'updated_at')
    if (updatedMs != null) body.updatedAt = updatedMs
    body.createdAt = parsePublishedAtMs(createdIso)
  }

  await updatePage(appId, pid, body)
  console.log(`ok slug:${slugForLookup}${item.note ? ` (${item.note})` : ''}`)
  return { ok: true, key: `slug:${slugForLookup}` }
}

async function main() {
  const cli = resolveCli(process.argv)

  if (!cli.appId) {
    console.error(
      'Set BILD_APP_ID or pass app id:\n  BILD_APP_ID=<id> node scripts/upsert-blog-metadata-from-hubspot-csv.js [/path/to.csv]'
    )
    process.exit(1)
  }
  if (!cli.csvPathAbs) {
    console.error(
      `No CSV path. Pass [/path/export.csv], set HUBSPOT_BLOG_CSV to an existing file, or place:\n  ${DEFAULT_HUBSPOT_DOWNLOAD}`
    )
    process.exit(1)
  }

  /** @type {Map<string, Record<string, string>>} */
  const rowsBySlug = new Map()
  for (const r of parseBlogCsvFile(cli.csvPathAbs)) {
    const k = normalizeContentSlug(r.slug ?? '')
    if (k) rowsBySlug.set(k, r)
  }

  const work = buildWorklist(rowsBySlug, cli.flags)

  /** @type {{ ok: boolean; key: string; detail?: string }[]} */
  const log = []
  for (const item of work) {
    const key = `slug:${item.slug}`
    try {
      const merged = await upsertOne(
        cli.appId,
        item,
        {
          dryRun: cli.flags.dryRun,
          publishFromCsv: cli.flags.publishFromCsv,
          alsoCreatedAt: cli.flags.alsoCreatedAt,
        },
        cli.csvPathAbs
      )
      log.push({ ok: merged.ok, key: merged.key })
    } catch (e) {
      const msg = e && typeof e.message === 'string' ? e.message : String(e)
      console.error(`fail ${key}: ${msg}`)
      log.push({ ok: false, key, detail: msg })
    }
  }

  const fails = log.filter((x) => !x.ok)
  if (fails.length) {
    console.error(`\n${fails.length} update(s) failed.`)
    process.exit(1)
  }
}

main().catch((e) => {
  console.error(e)
  process.exit(1)
})
