const fs = require('fs')
const path = require('path')
const { parse } = require('csv-parse/sync')

/** Headers expected (normalized from the CSV header row). */
const BLOG_CSV_COLUMNS = [
  'slug',
  'title',
  'published_at',
  'excerpt',
  'content',
  'category',
  'tags',
  'author',
  'image',
  'meta_title',
  'meta_description',
]

function normalizeHeader(h) {
  return String(h ?? '')
    .trim()
    .toLowerCase()
    .replace(/\s+/g, '_')
}

/**
 * Parse tags: comma-separated list, or a JSON string array.
 * @param {unknown} value
 * @returns {string[]}
 */
function parseTagsField(value) {
  const raw = String(value ?? '').trim()
  if (!raw) return []
  if (raw.startsWith('[')) {
    try {
      const parsed = JSON.parse(raw)
      if (Array.isArray(parsed)) {
        return parsed.map((t) => String(t).trim()).filter(Boolean)
      }
    } catch {
      // fall through
    }
  }
  return raw
    .split(',')
    .map((t) => t.trim())
    .filter(Boolean)
}

const CONTENT_SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/

function normalizeContentSlug(raw) {
  let s = String(raw ?? '').trim().toLowerCase()
  s = s.replace(/_/g, '-')
  s = s.replace(/[^a-z0-9-]+/g, '-')
  s = s.replace(/-+/g, '-').replace(/^-|-$/g, '')
  return s
}

function isValidContentSlug(slug) {
  return CONTENT_SLUG_PATTERN.test(slug)
}

function deriveContentPublication(row, forceDraft) {
  if (forceDraft) return { status: 'draft' }
  const raw = String(row.published_at ?? '').trim()
  if (!raw) return { status: 'draft' }
  const ts = Date.parse(raw)
  if (Number.isNaN(ts)) return { status: 'draft' }
  return {
    status: 'published',
    schedule: { persistent: true, startDate: ts },
  }
}

function derivePagePublication(row, forceDraft) {
  if (forceDraft) return { published: false }
  const raw = String(row.published_at ?? '').trim()
  if (!raw) return { published: false }
  const ts = Date.parse(raw)
  if (Number.isNaN(ts)) return { published: false }
  return {
    published: true,
    schedule: { persistent: true, startDate: ts },
  }
}

/**
 * Parse a blog/CMS `published_at` value to unix milliseconds for {@link mergePageScheduleWithPublishedAt}.
 * @param {string|number|Date} raw
 * @returns {number}
 */
function parsePublishedAtMs(raw) {
  if (raw instanceof Date) return raw.getTime()
  if (typeof raw === 'number' && Number.isFinite(raw)) return raw
  const s = String(raw ?? '').trim()
  if (!s) throw new Error('Published date is empty')
  const ts = Date.parse(s)
  if (Number.isNaN(ts)) throw new Error(`Invalid published date: ${JSON.stringify(String(raw))}`)
  return ts
}

/**
 * CMS page visibility schedule for a single persistent publish instant (`schedule.startDate` in ms).
 * Preserves unknown keys on existing schedule objects if present (e.g. future CMS fields).
 *
 * @param {Record<string, unknown>|null|undefined} existingSchedule
 * @param {string|number|Date} publishedAtRaw — ISO-like string, millis, or Date
 * @returns {Record<string, unknown>}
 */
function mergePageScheduleWithPublishedAt(existingSchedule, publishedAtRaw) {
  const ts = parsePublishedAtMs(publishedAtRaw)
  const base =
    existingSchedule && typeof existingSchedule === 'object' && !Array.isArray(existingSchedule)
      ? { ...existingSchedule }
      : {}
  return { ...base, persistent: true, startDate: ts }
}

/**
 * Page metaTags shape matches CMS PageForm (title, description, keywords, ogTitle, ogDescription, ogImage).
 * @param {Record<string, string>} row
 * @param {{ resolvedOgImageUrl?: string }} [opts]
 * @returns {Record<string, string>|undefined}
 */
function metaTagsFromCsvRow(row, opts = {}) {
  const title = String(row.title ?? '').trim()
  const metaTitle = String(row.meta_title ?? '').trim()
  const metaDesc = String(row.meta_description ?? '').trim()
  const excerpt = String(row.excerpt ?? '').trim()
  const tags = String(row.tags ?? '').trim()

  /** @type {Record<string, string>} */
  const mt = {}
  const seoTitle = metaTitle || title
  if (seoTitle) mt.title = seoTitle
  if (metaDesc) mt.description = metaDesc
  if (tags) mt.keywords = tags
  const ogTitle = metaTitle || title
  if (ogTitle) mt.ogTitle = ogTitle
  const ogDesc = metaDesc || excerpt
  if (ogDesc) mt.ogDescription = ogDesc
  const ogImgRaw =
    opts.resolvedOgImageUrl !== undefined ? opts.resolvedOgImageUrl : String(row.image ?? '').trim()
  const ogImg = String(ogImgRaw ?? '').trim()
  if (ogImg) mt.ogImage = ogImg

  return Object.keys(mt).length > 0 ? mt : undefined
}

/**
 * POST/PUT `/api/apps/:id/pages` body (links `contentId` to HTML content record).
 * Optional CSV column `page_slug`: URL slug for the page; defaults to `slug` (content slug).
 *
 * @param {Record<string, string>} row
 * @param {{ contentId: string, forceDraft?: boolean, resolvedOgImageUrl?: string }} opts
 * @returns {Record<string, unknown>}
 */
function csvRowToPageBody(row, opts) {
  const pageSlugRaw = String(row.page_slug ?? row.slug ?? '').trim()
  const pageSlug = normalizeContentSlug(pageSlugRaw)
  const name = String(row.title ?? '').trim()
  const pub = derivePagePublication(row, Boolean(opts.forceDraft))
  const metaTags = metaTagsFromCsvRow(row, { resolvedOgImageUrl: opts.resolvedOgImageUrl })

  /** @type {Record<string, unknown>} */
  const payload = {
    slug: pageSlug,
    name,
    contentId: opts.contentId,
    published: pub.published,
  }
  if (pub.schedule) payload.schedule = pub.schedule
  if (metaTags) payload.metaTags = metaTags

  const excerpt = String(row.excerpt ?? '').trim()
  const metaDesc = String(row.meta_description ?? '').trim()
  const preview = excerpt || metaDesc
  if (preview) payload.previewDescription = preview

  return omitUndefined(payload)
}

/**
 * CMS `code` is the HTML body string (what `/remote-content` and the editor expose), not JSON.
 * @param {Record<string, string>} row
 * @returns {string}
 */
function htmlCodeFromCsvRow(row) {
  const raw = row.content
  if (raw == null) return ''
  const str = typeof raw === 'string' ? raw : String(raw)
  return str.replace(/^\uFEFF/, '')
}

/**
 * Map a CSV row to POST/PUT `/api/apps/:id/contents` body.
 * Only maps fields the CMS content API uses: name, slug, devices, code (HTML), status/schedule.
 * Excerpt, category, tags, author, image, meta_* remain in the CSV for your records but are not sent.
 *
 * @param {Record<string, string>} row
 * @param {{ devices?: string[], forceDraft?: boolean }} [opts]
 * @returns {Record<string, unknown>}
 */
function csvRowToContentBody(row, opts = {}) {
  const devices = opts.devices && opts.devices.length ? opts.devices : ['web']
  const slug = normalizeContentSlug(row.slug)
  const name = String(row.title ?? '').trim()
  const code = htmlCodeFromCsvRow(row)

  /** @type {Record<string, unknown>} */
  const payload = {
    name,
    slug,
    devices,
    code,
    ...deriveContentPublication(row, Boolean(opts.forceDraft)),
  }

  return omitUndefined(payload)
}

/**
 * Strip keys whose values are undefined (axios JSON bodies).
 * @param {Record<string, unknown>} obj
 * @returns {Record<string, unknown>}
 */
function omitUndefined(obj) {
  /** @type {Record<string, unknown>} */
  const out = {}
  for (const [k, v] of Object.entries(obj)) {
    if (v !== undefined) out[k] = v
  }
  return out
}

/**
 * @param {string} content — raw CSV text
 * @param {{ delimiter?: string }} [options]
 * @returns {Record<string, string>[]}
 */
function parseBlogCsv(content, options = {}) {
  const delimiter = options.delimiter || ','
  const records = parse(content, {
    columns: (header) => header.map(normalizeHeader),
    skip_empty_lines: true,
    trim: true,
    delimiter,
    relax_column_count: true,
    relax_quotes: true,
  })
  return records
}

/**
 * @param {string} filePath
 * @param {{ delimiter?: string }} [options]
 * @returns {Record<string, string>[]}
 */
function parseBlogCsvFile(filePath, options = {}) {
  const abs = path.resolve(filePath)
  const content = fs.readFileSync(abs, 'utf8')
  return parseBlogCsv(content, options)
}

/**
 * @param {Record<string, string>} sampleRow — first data row or synthetic keys row
 * @returns {{ ok: boolean, missing: string[] }}
 */
function validateBlogCsvColumns(sampleRow) {
  const keys = new Set(Object.keys(sampleRow))
  const missing = BLOG_CSV_COLUMNS.filter((c) => !keys.has(c))
  return { ok: missing.length === 0, missing }
}

function resolveImagePathRelativeToCsv(csvPath, imageCell) {
  const cell = String(imageCell ?? '').trim()
  if (!cell) return cell
  if (cell.startsWith('http://') || cell.startsWith('https://') || cell.startsWith('data:')) {
    return cell
  }
  const base = path.resolve(path.dirname(path.resolve(csvPath)))
  return path.resolve(base, cell)
}

module.exports = {
  BLOG_CSV_COLUMNS,
  CONTENT_SLUG_PATTERN,
  parseBlogCsv,
  parseBlogCsvFile,
  parseTagsField,
  csvRowToContentBody,
  csvRowToPageBody,
  metaTagsFromCsvRow,
  derivePagePublication,
  parsePublishedAtMs,
  mergePageScheduleWithPublishedAt,
  htmlCodeFromCsvRow,
  normalizeContentSlug,
  isValidContentSlug,
  omitUndefined,
  validateBlogCsvColumns,
  resolveImagePathRelativeToCsv,
}
