const { listPages, getPage, updatePage } = require('./api')
const { mergePageScheduleWithPublishedAt, parsePublishedAtMs } = require('./blog-csv')

function coerceAuditTimestampToUnixMs(existing, ...keys) {
  for (const key of keys) {
    const v = existing[key]
    if (typeof v === 'number' && Number.isFinite(v)) return v
    if (v && typeof v === 'object' && typeof v.seconds === 'number') {
      const ns = typeof v.nanoseconds === 'number' ? v.nanoseconds : 0
      return Math.floor(v.seconds * 1000 + ns / 1e6)
    }
    if (typeof v === 'string' && Date.parse(v)) return Date.parse(v)
  }
  return undefined
}

function normalizePagesListResponse(data) {
  if (Array.isArray(data)) return data
  if (data && typeof data === 'object' && Array.isArray(data.data)) return data.data
  return []
}

/**
 * Resolve CMS page id from --page-id or by listing with --slug.
 * @param {{ appId: string, pageId?: string, slug?: string }} args
 * @returns {Promise<string>}
 */
async function resolvePageIdByLookup(args) {
  const { appId, pageId, slug } = args
  const pid = pageId != null ? String(pageId).trim() : ''
  if (pid) return pid

  const s = slug != null ? String(slug).trim() : ''
  if (!s) {
    throw new Error('Provide --page-id or --slug to identify the page')
  }

  const raw = await listPages(appId, { slug: s })
  const pages = normalizePagesListResponse(raw)
  const matches = pages.filter((p) => p && String(p.slug).trim() === s)

  if (matches.length === 0) {
    throw new Error(`No page with slug "${s}"`)
  }
  if (matches.length > 1) {
    throw new Error(`Multiple pages with slug "${s}"; use --page-id`)
  }
  return String(matches[0].id)
}

/**
 * @param {Record<string, unknown>} opts — camelCase CLI option names
 * @returns {Record<string, string>|undefined}
 */
function metaTagsFromCliOptions(opts) {
  const pairs = [
    ['metaTitle', 'title'],
    ['metaDescription', 'description'],
    ['keywords', 'keywords'],
    ['ogTitle', 'ogTitle'],
    ['ogDescription', 'ogDescription'],
    ['ogImage', 'ogImage'],
  ]
  /** @type {Record<string, string>} */
  const mt = {}
  for (const [key, mtKey] of pairs) {
    const v = opts[key]
    if (v != null && String(v).trim() !== '') mt[mtKey] = String(v).trim()
  }
  return Object.keys(mt).length > 0 ? mt : undefined
}

/**
 * @param {unknown} existing
 * @param {Record<string, string>|undefined} patch
 * @returns {Record<string, string>|undefined}
 */
function mergePageMetaTags(existing, patch) {
  if (!patch || typeof patch !== 'object') {
    if (existing && typeof existing === 'object' && !Array.isArray(existing)) return { ...existing }
    return undefined
  }
  const base =
    existing && typeof existing === 'object' && !Array.isArray(existing) ? { ...existing } : {}
  return { ...base, ...patch }
}

/**
 * Merge CMS SEO blobs into one object for PUT `metaTags`. Later layers override earlier ones.
 *
 * @param {Record<string, unknown>} existing
 * @returns {Record<string, unknown>|undefined}
 */
function normalizeMetaTagsForPut(existing) {
  /** @type {Record<string, unknown>} */
  const out = {}
  /** @type {unknown[]} */
  const layers = [
    existing.seo,
    existing.meta,
    existing.meta_tags,
    existing.metaTags,
    existing.metaTagsJson,
    existing.meta_tags_json,
  ]
  let hit = false
  for (const layer of layers) {
    if (layer && typeof layer === 'object' && !Array.isArray(layer)) {
      hit = true
      Object.assign(out, layer)
    }
  }
  return hit ? out : undefined
}

/**
 * Preview / excerpt fields sometimes arrive under alternate keys from GET.
 *
 * @param {Record<string, unknown>} existing
 * @returns {string|undefined}
 */
function normalizePreviewDescriptionForPut(existing) {
  const candidates = [
    existing.previewDescription,
    existing.preview_description,
    existing.excerpt,
    existing.previewText,
    existing.preview_text,
  ]
  for (const c of candidates) {
    if (c != null && String(c).trim() !== '') return String(c).trim()
  }
  return undefined
}

/**
 * Baseline PUT body from an existing CMS page (matches `pages update` defaults: keeps schedule/metaTags when unchanged).
 *
 * @param {Record<string, unknown>} existing
 * @returns {Record<string, unknown>}
 */
function pagePutPayloadFromExisting(existing) {
  /** @type {Record<string, unknown>} */
  const body = {
    slug: existing.slug != null ? String(existing.slug) : '',
    name: existing.name != null ? String(existing.name) : '',
    contentId: existing.contentId != null ? String(existing.contentId) : '',
    published: existing.published === true,
    archived: existing.archived === true,
    deleted: existing.deleted === true,
  }
  if (existing.schedule && typeof existing.schedule === 'object') {
    body.schedule = { ...existing.schedule }
  }

  const mt = normalizeMetaTagsForPut(existing)
  if (mt && Object.keys(mt).length > 0) {
    body.metaTags = mt
  }

  const preview = normalizePreviewDescriptionForPut(existing)
  if (preview !== undefined) body.previewDescription = preview

  return body
}

/**
 * Set or clear the CMS page publish instant (`schedule.startDate`, persistent blog-style).
 *
 * @param {string} appId
 * @param {{ pageId?: string, slug?: string }} lookup — exactly one identifiers path like `resolvePageIdByLookup`
 * @param {string|number|Date|null} publishedAt — new date/time, or `null` to remove `schedule` from the PUT body
 * @returns {Promise<unknown>} CMS API response body
 */
async function updatePagePublishDate(appId, lookup, publishedAt) {
  const pid = await resolvePageIdByLookup({
    appId,
    pageId: lookup.pageId != null ? String(lookup.pageId).trim() : undefined,
    slug: lookup.slug != null ? String(lookup.slug).trim() : undefined,
  })
  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)
  if (publishedAt === null) {
    delete body.schedule
  } else {
    body.schedule = mergePageScheduleWithPublishedAt(existing.schedule, publishedAt)
  }
  return updatePage(appId, pid, body)
}

/**
 * Set CMS page `createdAt` (epoch milliseconds). Leaves publish `schedule` unchanged.
 * Reads `updatedAt` from the existing payload when needed to echo it back on PUT (camelCase on wire).
 *
 * @param {string} appId
 * @param {{ pageId?: string, slug?: string }} lookup
 * @param {string|number|Date} createdAtRaw — same parsing as CSV `published_at`
 * @returns {Promise<unknown>}
 */
async function updatePageCreatedAt(appId, lookup, createdAtRaw) {
  const pid = await resolvePageIdByLookup({
    appId,
    pageId: lookup.pageId != null ? String(lookup.pageId).trim() : undefined,
    slug: lookup.slug != null ? String(lookup.slug).trim() : undefined,
  })
  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 updatedMs = coerceAuditTimestampToUnixMs(existing, 'updatedAt', 'updated_at')
  if (updatedMs != null) body.updatedAt = updatedMs
  body.createdAt = parsePublishedAtMs(createdAtRaw)
  return updatePage(appId, pid, body)
}

module.exports = {
  normalizePagesListResponse,
  resolvePageIdByLookup,
  metaTagsFromCliOptions,
  mergePageMetaTags,
  normalizeMetaTagsForPut,
  normalizePreviewDescriptionForPut,
  pagePutPayloadFromExisting,
  updatePagePublishDate,
  updatePageCreatedAt,
  coerceAuditTimestampToUnixMs,
}
