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

const { getConfig } = require('../lib/config')
const { resolveCmsDependenciesFilePath } = require('../lib/cms-deps-path')
const { patchCmsDependenciesFile } = require('../lib/cms-deps-patcher')
const { mergeBarrelReexportLines } = require('../lib/cms-dependencies-boilerplate')
const { runCodemodOnFile } = require('../lib/codemod')
const { findSsrHydrationRisks } = require('../lib/ssr-hydration-patterns')
const { preprocessBilditTemplate, hasBilditTemplateVars } = require('../lib/preprocessBilditTemplate')
const webTranspile = require('../lib/transpile')
const { lint } = require('../lib/lint')
const {
  extractTemplateIdFromPath,
  formatTemplateVersionBannerLine,
  nextTemplateVersionDisplay,
  parseTopTemplateVersionComment,
  prependTemplateVersionBanner,
  totalTaggedFieldCount,
} = require('../lib/template-meta')

function defaultDepsHeader(cliName) {
  return (
    `// AUTO-GENERATED: managed by \`bild template ${cliName}\`. Hand-edit with care.\n` +
    '// Symbols in this file are referenced by BILDIT CMS Templates.\n\n'
  )
}

function assertTypescriptSource(absPath, cliName) {
  const e = path.extname(absPath).toLowerCase()
  if (e !== '.ts' && e !== '.tsx') {
    throw new Error(`${cliName} requires a .ts or .tsx source file (got ${e || 'no extension'})`)
  }
}

function ensureDepsFileExists(depsFilePath, cliName) {
  if (fs.existsSync(depsFilePath)) return
  fs.mkdirSync(path.dirname(depsFilePath), { recursive: true })
  fs.writeFileSync(depsFilePath, defaultDepsHeader(cliName), 'utf8')
}

function depsReportHasPatch(depsReport) {
  if (!depsReport || !depsReport.depsPatch) return false
  const { namespaceImports, recordEntries } = depsReport.depsPatch
  return (namespaceImports && namespaceImports.length > 0) || (recordEntries && recordEntries.length > 0)
}

/**
 * Writes structured cmsDependencies updates (namespace imports + record) and components barrel re-exports.
 */
async function applyDepsFileUpdates(depsFilePath, depsReport, cliName) {
  if (!depsFilePath || !depsReport) return
  if (depsReportHasPatch(depsReport)) {
    ensureDepsFileExists(depsFilePath, cliName)
    await patchCmsDependenciesFile(depsFilePath, depsReport.depsPatch)
  }
  if (
    depsReport.useBarrelReexports &&
    depsReport.barrelFilePath &&
    depsReport.barrelReexportLines &&
    depsReport.barrelReexportLines.length
  ) {
    mergeBarrelReexportLines(depsReport.barrelFilePath, depsReport.barrelReexportLines)
  }
}

function printReport({ outPath, result, depsFilePath, dryRun }) {
  const { cssReport, depsReport, fieldReport } = result
  console.log(chalk.blue('\nSummary'))
  console.log(`  CSS inlined rules : ${cssReport.inlinedRules}`)
  if (cssReport.styleTagSize) {
    console.log(`  CSS in <style>    : ${cssReport.styleTagSize} bytes`)
  }
  if (cssReport.warnings.length) {
    cssReport.warnings.forEach((w) => console.log(chalk.yellow(`  CSS warn: ${w}`)))
  }
  console.log(`  Moved imports     : ${depsReport.moved.length}`)
  depsReport.moved.forEach((m) => console.log(`    - ${m}`))
  if (depsReport.conflicts.length) {
    depsReport.conflicts.forEach((c) =>
      console.log(chalk.yellow(`  Deps conflict: ${c.name} (from ${c.from}) already in deps file; skipped`)),
    )
  }
  if (depsReport.warnings.length) {
    depsReport.warnings.forEach((w) => console.log(chalk.yellow(`  Deps warn: ${w}`)))
  }
  console.log(
    `  Template fields   : RichText=${fieldReport.richTextCount}, Color=${fieldReport.colorCount}, String=${fieldReport.stringCount}, Boolean=${fieldReport.booleanCount || 0}, Number=${fieldReport.numberCount || 0}, Image=${fieldReport.imageCount || 0}`,
  )
  console.log('')
  console.log(`  ${dryRun ? 'Would write' : 'Wrote'} component: ${outPath}`)
  if (
    depsReportHasPatch(depsReport) ||
    (depsReport.useBarrelReexports && depsReport.barrelReexportLines && depsReport.barrelReexportLines.length)
  ) {
    console.log(`  ${dryRun ? 'Would update' : 'Updated'} cms deps: ${depsFilePath}`)
  } else if (depsFilePath) {
    console.log(`  No cmsDependencies file changes`)
  }
}

function toTemplatePath(inputPath) {
  const dir = path.dirname(inputPath)
  const ext = path.extname(inputPath)
  const base = path.basename(inputPath, ext)
  const cleanBase = base.endsWith('.template') ? base : `${base}.template`
  return path.join(dir, `${cleanBase}${ext}`)
}

/**
 * @param {string} absPath
 * @param {string} source
 * @returns {string | null} Human-readable reason to skip from-jsx/from-tsx, or null if OK
 */
function templateFromJsxBlockedReason(absPath, source) {
  const base = path.basename(absPath)
  const ext = path.extname(base)
  const stem = ext ? base.slice(0, -ext.length) : base
  if (stem.endsWith('.template')) {
    return 'path looks like a template output (e.g. *.template.tsx)'
  }
  if (hasBilditTemplateVars(source)) {
    return 'source already contains BILDIT $(…) template variables'
  }
  return null
}

/**
 * V2 `from-jsx` / `from-tsx` output must match repo Prettier so CI/Vercel checks pass.
 * Prettier 3+ exposes async `resolveConfig` / `format` from the CJS entry (no `resolveConfigSync`).
 * @param {string} code
 * @param {string} filepathForConfig - path used for parser + .prettierrc resolution
 */
async function formatV2OutputWithPrettier(code, filepathForConfig) {
  const config = (await prettier.resolveConfig(filepathForConfig)) || {}
  return prettier.format(code, {
    ...config,
    filepath: filepathForConfig,
  })
}

/**
 * @param {string} file
 * @param {object} opts
 * @param {'lineComments'|'jsdoc'} opts.groupingStyle - v1-style // group or v2-style // @template v2 + @group/@type JSDoc
 * @param {string} [opts.cliPrefix] - e.g. 'legacy ' or ''
 */
async function templateFromJsxCommand(file, opts = {}) {
  const groupingStyle = opts.groupingStyle || 'lineComments'
  const cliName = opts.cliName || 'from-jsx'
  const logLabel = opts.logLabel || cliName
  try {
    if (!file) throw new Error('file argument is required')
    const absInput = path.resolve(file)
    if (!fs.existsSync(absInput)) throw new Error(`File not found: ${file}`)
    if (opts.typescriptOnly) assertTypescriptSource(absInput, cliName)

    const config = getConfig()
    const cssMode = opts.cssMode || config.cssMode || 'hybrid'
    const namingStrategy = opts.naming || config.namingStrategy || 'camelComponent'
    const depsImportAlias =
      opts.depsAlias || config.cmsDependenciesImportAlias || '@/cmsDependencies'
    const depsFilePath = resolveCmsDependenciesFilePath({ ...opts, config })

    const outPath =
      opts.out === '-' ? '-' : path.resolve(opts.out || toTemplatePath(absInput))
    const fieldMapPath = path.resolve(
      path.dirname(absInput),
      '.bild',
      `${path.basename(absInput).replace(/\.(jsx?|tsx?)$/, '')}.field-map.json`,
    )

    const inputSource = fs.readFileSync(absInput, 'utf8')
    const blocked = templateFromJsxBlockedReason(absInput, inputSource)
    if (blocked) {
      throw new Error(
        `Already a BILDIT template (${blocked}). \`${cliName}\` only converts plain components — edit this file directly, or use a non-template source (no *.template.* name, no $() fields).`,
      )
    }

    if (opts.ssrCheck !== false) {
      const { issues: ssrIssues } = findSsrHydrationRisks(inputSource, absInput)
      if (ssrIssues.length) {
        console.log(chalk.yellow('\nSSR / hydration risks in source (can cause server vs client HTML mismatch):'))
        ssrIssues.forEach((i) => {
          console.log(chalk.yellow(`  Line ${i.line}: ${i.code}`))
          console.log(chalk.dim(`    ${i.hint}`))
        })
        if (opts.strict) {
          throw new Error('Strict mode failed: fix SSR/hydration risks in source (see above)')
        }
      }
    }

    console.log(chalk.blue(`Rewriting ${path.basename(absInput)}...`))
    const result = runCodemodOnFile(absInput, {
      cssMode,
      namingStrategy,
      depsImportAlias,
      depsFilePath,
      fieldMapPath,
      groupingStyle,
      sitePath: opts.sitePath,
    })

    if (groupingStyle === 'jsdoc') {
      const prettierPath = outPath === '-' ? absInput : outPath
      try {
        result.output = await formatV2OutputWithPrettier(result.output, prettierPath)
      } catch (e) {
        throw new Error(`Prettier format failed: ${e.message}`)
      }
    }

    const templateIdForBanner = extractTemplateIdFromPath(absInput)
    let priorVer = null
    if (outPath !== '-' && fs.existsSync(outPath)) {
      priorVer = parseTopTemplateVersionComment(fs.readFileSync(outPath, 'utf8'), templateIdForBanner)
    }
    if (priorVer == null) {
      priorVer = parseTopTemplateVersionComment(inputSource, templateIdForBanner)
    }
    const nextVerDisplay = nextTemplateVersionDisplay(priorVer)
    const isLegacyCodemod = groupingStyle === 'lineComments'
    const versionBannerLine = formatTemplateVersionBannerLine(
      templateIdForBanner,
      nextVerDisplay,
      isLegacyCodemod,
    )
    result.output = prependTemplateVersionBanner(result.output, versionBannerLine)

    if (totalTaggedFieldCount(result.fieldReport) === 0) {
      console.log(
        chalk.yellow(
          '\nWarning: no template variables were found (no v1 $(…) syntax or v2 JSDoc @type fields). The CMS will not expose editable fields for this template unless you add them.\n',
        ),
      )
    }

    if (!opts.dryRun) {
      if (outPath === '-') {
        process.stdout.write(result.output)
      } else {
        fs.mkdirSync(path.dirname(outPath), { recursive: true })
        fs.writeFileSync(outPath, result.output, 'utf8')
      }
      if (
        depsReportHasPatch(result.depsReport) ||
        (result.depsReport.useBarrelReexports && result.depsReport.barrelReexportLines && result.depsReport.barrelReexportLines.length)
      ) {
        if (!depsFilePath) {
          console.log(
            chalk.yellow(
              '  (skipped writing deps; no --deps-file or sitePath/cmsDependenciesFile configured)',
            ),
          )
        } else {
          await applyDepsFileUpdates(depsFilePath, result.depsReport, logLabel)
        }
      }
    }

    if (opts.strict || opts.verify !== false) {
      const preprocessed = preprocessBilditTemplate(result.output, result.codeType)
      try {
        void webTranspile(preprocessed, result.codeType, path.basename(absInput))
      } catch (e) {
        throw new Error(`Verify transpile failed: ${e.message}`)
      }
      if (opts.strict) {
        const lintResult = await lint(preprocessed, absInput)
        if (lintResult.errorCount > 0) {
          console.log(chalk.red(`\nLint errors (${lintResult.errorCount}) on generated output:`))
          lintResult.issues
            .filter((i) => i.severity === 'error')
            .forEach((i) => console.log(chalk.red(`  Line ${i.line}: ${i.message}`)))
          throw new Error('Strict mode failed: lint errors on generated output (see above)')
        }
      }
    }

    printReport({
      outPath: outPath === '-' ? '<stdout>' : outPath,
      result,
      depsFilePath,
      dryRun: !!opts.dryRun,
    })

    if (groupingStyle === 'jsdoc') {
      console.log(
        chalk.dim(
          'V2 style: // @template v2 plus per-field /** @group … @type … */ JSDoc; consts use plain JS literals. ' +
            'Legacy uses // group / // endgroup and $() template syntax.',
        ),
      )
    }

    console.log(chalk.green('\nDone.'))
  } catch (error) {
    console.error(chalk.red(`\n${logLabel} failed: ${error.message}`))
    process.exit(1)
  }
}

module.exports = {
  templateFromJsxCommand,
  toTemplatePath,
  defaultDepsHeader,
  applyDepsFileUpdates,
  depsReportHasPatch,
  ensureDepsFileExists,
  templateFromJsxBlockedReason,
}
