const fs = require('fs')
const path = require('path')
const chalk = require('chalk')
const parser = require('@babel/parser')
const traverseMod = require('@babel/traverse')
const generateMod = require('@babel/generator')

const traverse = traverseMod.default || traverseMod
const generate = generateMod.default || generateMod

const {
  preprocessBilditTemplate,
  canonicalizeDoubleParenAsCastTemplates,
} = require('./preprocessBilditTemplate')

const { getConfig } = require('./config')
const {
  parserPluginsFor,
  normalizeTSTypeCastExpressionsForGenerator,
  programHasTSTypeCastExpression,
} = require('./codemod/utils')
const { extractDependencies } = require('./codemod/deps-extractor')
const { resolveCmsDependenciesFilePath, inferSitePathFromFile } = require('./cms-deps-path')
const { formatDepsPatchForDisplay } = require('./cms-deps-patcher')
const {
  DEFAULT_COMPONENTS_IMPORT_PATH,
  sitePathMustBeDirectory,
  resolveDefaultComponentsBarrelOnDisk,
} = require('./cms-dependencies-boilerplate')

function loadSiteBildJsonConfig(sitePath) {
  if (!sitePath) return null
  const filePath = path.join(sitePath, 'bild.json')
  try {
    if (!fs.existsSync(filePath)) return null
    const raw = fs.readFileSync(filePath, 'utf8')
    const parsed = JSON.parse(raw)
    if (!parsed || typeof parsed !== 'object') return null
    return parsed
  } catch (_e) {
    return null
  }
}

function resolveConfigAndSitePathForDeps(filePath, opts = {}) {
  const baseConfig = opts.config || getConfig()
  const inferredSitePath = filePath ? inferSitePathFromFile(filePath) : null
  const sitePath = opts.sitePath || baseConfig.sitePath || inferredSitePath || null
  const siteConfig = loadSiteBildJsonConfig(sitePath)
  const config = siteConfig ? { ...baseConfig, ...siteConfig } : baseConfig
  return { config, sitePath }
}

function detectParserCodeType(filePath) {
  const ext = path.extname(filePath).toLowerCase()
  if (ext === '.tsx' || ext === '.ts') return 'tsx'
  if (ext === '.jsx' || ext === '.js') return 'jsx'
  return null
}

/**
 * When sitePath is a valid project root, re-export local imports via the Components barrel.
 * Inlining into cmsDependencies is not supported.
 */
function resolveBarrelOptsForLibrary(opts = {}) {
  const config = opts.config || getConfig()
  const site = opts.sitePath || config.sitePath
  if (!site) {
    return {
      reexportLocalImportsViaBarrel: false,
      barrelFilePath: null,
      componentsImportPath: null,
    }
  }
  const d = sitePathMustBeDirectory(site)
  if (!d.ok) {
    return {
      reexportLocalImportsViaBarrel: false,
      barrelFilePath: null,
      componentsImportPath: null,
    }
  }
  const b = resolveDefaultComponentsBarrelOnDisk(d.abs)
  return {
    reexportLocalImportsViaBarrel: true,
    barrelFilePath: b.barrelAbs,
    componentsImportPath: opts.componentsImportPath || config.componentsImportPath || DEFAULT_COMPONENTS_IMPORT_PATH,
  }
}

/**
 * Same dependency analysis as `bild template legacy from-jsx|from-tsx` (deps extractor only).
 * Use to show what would be appended to cmsDependencies and which imports would move.
 *
 * @returns {null | { report: object, depsFilePath: string|null, depsImportAlias: string }}
 */
function analyzeLibraryDeps(filePath, opts = {}) {
  const parserCt = detectParserCodeType(filePath)
  if (!parserCt) {
    return null
  }

  const { config, sitePath } = resolveConfigAndSitePathForDeps(filePath, opts)
  const sourceRaw = fs.readFileSync(filePath, 'utf8')
  // Templates can contain legacy `$(field:Type=default)` expressions which are not valid JS/TS.
  // Preprocess those to defaults/null before running Babel so deps hinting works on legacy templates.
  const source = preprocessBilditTemplate(
    canonicalizeDoubleParenAsCastTemplates(sourceRaw, parserCt),
    parserCt,
  )
  const ast = parser.parse(source, {
    sourceType: 'module',
    plugins: parserPluginsFor(parserCt),
    errorRecovery: true,
  })

  const depsImportAlias =
    opts.depsImportAlias || config.cmsDependenciesImportAlias || '@/cmsDependencies'
  const depsFilePath = resolveCmsDependenciesFilePath({
    depsFile: opts.depsFile,
    sitePath,
    config,
  })

  const barrelOpts = resolveBarrelOptsForLibrary({ ...opts, config, sitePath })
  const report = extractDependencies({
    ast,
    traverse,
    generate,
    filePath,
    depsImportAlias,
    depsFilePath,
    reexportLocalImportsViaBarrel: barrelOpts.reexportLocalImportsViaBarrel,
    barrelFilePath: barrelOpts.barrelFilePath,
    componentsImportPathForBarrel: barrelOpts.componentsImportPath,
  })

  return { report, depsFilePath, depsImportAlias }
}

function printLibraryDepsHints(analysis, sourceFilePath) {
  if (!analysis) {
    return
  }

  const { report, depsFilePath, depsImportAlias } = analysis
  const hasMoved = report.moved && report.moved.length > 0
  const hasDepsPatch =
    report.depsPatch &&
    ((report.depsPatch.namespaceImports && report.depsPatch.namespaceImports.length > 0) ||
      (report.depsPatch.recordEntries && report.depsPatch.recordEntries.length > 0))
  const hasExtras =
    hasMoved ||
    hasDepsPatch ||
    (report.conflicts && report.conflicts.length) ||
    (report.warnings && report.warnings.length)

  if (!hasExtras) {
    const uniqueKept = [...new Set(report.kept || [])]
    console.log(chalk.cyan('\n── cmsDependencies (import hints) ──'))
    if (!depsFilePath) {
      console.log(
        chalk.dim(
          'No deps file path: it only uses your cmsDependencies import alias, or the deps file path is unset. ' +
            'Set `--deps-file` / `sitePath` so npm packages can be registered, and set `sitePath` so local / @/ imports use the Components barrel.',
        ),
      )
    } else {
      const exists = fs.existsSync(depsFilePath)
      console.log(
        chalk.dim(
          `No cmsDependencies changes suggested for this file. Suggested deps file: ${depsFilePath}${
            exists ? '' : ' (create with template command or manually)'
          }`,
        ),
      )
    }
    if (uniqueKept.length) {
      console.log(chalk.dim(`  Imports left as written: ${uniqueKept.join(', ')}`))
    }
    return
  }

  const quotedSource = JSON.stringify(sourceFilePath || 'your-file.tsx')

  console.log(chalk.cyan('\n── cmsDependencies (import hints) ──'))
  console.log(
    chalk.dim(
      'By default, library add and library update upload your file unchanged. Use --apply-deps (or confirm when prompted) to: register npm packages on your cms dependencies object (`import * as Pkg` + record key) while keeping `from "pkg"` in the source, and re-export local / @/ imports through `app/components/Components` when `sitePath` is set.',
    ),
  )

  if (depsFilePath) {
    const exists = fs.existsSync(depsFilePath)
    console.log(chalk.dim(`Suggested deps file: ${depsFilePath}${exists ? '' : ' (create with template command or manually)'}`))
  } else {
    console.log(
      chalk.yellow(
        'No deps file path: set `sitePath` in ~/.bild.json, or use --site-path / --deps-file to see conflicts with an on-disk file.',
      ),
    )
  }
  console.log(
    chalk.dim(
      `Import alias in generated code: ${depsImportAlias}` +
        (report.useBarrelReexports
          ? ' (local paths also merge into named imports from your Components barrel path)'
          : ''),
    ),
  )

  if (report.moved.length) {
    console.log(chalk.dim('\nSource paths merged through the Components barrel (when `sitePath` is set):'))
    report.moved.forEach((m) => console.log(chalk.dim(`  - ${m}`)))
  }

  if (report.useBarrelReexports && report.barrelReexportLines && report.barrelReexportLines.length) {
    console.log(
      chalk.dim(
        '\nWith a valid `sitePath`, add these re-exports to your Components barrel. Templates are rewritten to `import { … }` from the barrel; keep `import * as Components` and `@components` in cmsDependencies for the registry only.\n',
      ),
    )
    report.barrelReexportLines.forEach((line) => console.log(chalk.green(line)))
  }
  if (hasDepsPatch) {
    const hint = formatDepsPatchForDisplay(report.depsPatch)
    if (hint) {
      console.log(chalk.dim('\nSuggested cmsDependencies patch:\n'))
      console.log(chalk.green(hint))
    }
  }

  if (report.conflicts.length) {
    console.log(chalk.yellow('\nName conflicts with existing exports in the deps file (skipped in codemod):'))
    report.conflicts.forEach((c) =>
      console.log(chalk.yellow(`  - ${c.name} (from ${c.from})`)),
    )
  }

  if (report.warnings.length) {
    report.warnings.forEach((w) => console.log(chalk.yellow(`  Deps: ${w}`)))
  }

  console.log(
    chalk.dim(
      '\nTo rewrite imports the same way as the template pipeline, run e.g.\n' +
        `  bild template legacy from-tsx ${quotedSource} --deps-file <path> --out <file.template.tsx>\n` +
        'then add or update the template with `bild library add` / `bild library update` and `--apply-deps`.',
    ),
  )
}

function reportHasCmsDependencyWork(report) {
  if (!report) return false
  const hasDepsPatch =
    report.depsPatch &&
    ((report.depsPatch.namespaceImports && report.depsPatch.namespaceImports.length > 0) ||
      (report.depsPatch.recordEntries && report.depsPatch.recordEntries.length > 0))
  return (
    (report.moved && report.moved.length > 0) ||
    !!(report.useBarrelReexports && report.barrelReexportLines && report.barrelReexportLines.length) ||
    hasDepsPatch
  )
}

/**
 * True when a component has relative/@/ or external deps that the cmsDependencies codemod can merge
 * (same notion of “work” as before uploading with --apply-deps).
 */
function analysisHasCmsDepsWork(analysis) {
  return reportHasCmsDependencyWork(analysis && analysis.report)
}

/**
 * True when a deps file path is not resolved but the codemod has something to apply.
 * (Moved-only was too narrow: same work as `analysisHasCmsDepsWork` without a path.)
 */
function needsOptionalDepsFilePrompt(analysis) {
  if (!analysis || analysis.depsFilePath != null) {
    return false
  }
  return analysisHasCmsDepsWork(analysis)
}

/**
 * Rewrite imports to the cmsDependencies alias and compute exports to append (same as deps step of
 * `bild template legacy`). Does not write the deps file or the component — caller uploads `code` or writes disk.
 *
 * @returns {{ code: string, applied: boolean, report: object|null, depsFilePath: string|null }}
 */
function applyLibraryDepsRewrite(filePath, opts = {}) {
  const { config, sitePath } = resolveConfigAndSitePathForDeps(filePath, opts)
  const source = fs.readFileSync(filePath, 'utf8')
  const parserCt = detectParserCodeType(filePath)
  if (!parserCt) {
    return { code: source, applied: false, report: null, depsFilePath: null }
  }
  const ast = parser.parse(source, {
    sourceType: 'module',
    plugins: parserPluginsFor(parserCt),
    errorRecovery: true,
  })

  const depsImportAlias =
    opts.depsImportAlias || config.cmsDependenciesImportAlias || '@/cmsDependencies'
  const depsFilePath = resolveCmsDependenciesFilePath({
    depsFile: opts.depsFile,
    sitePath,
    config,
  })

  if (!depsFilePath) {
    return { code: source, applied: false, report: null, depsFilePath: null }
  }

  const barrelOpts = resolveBarrelOptsForLibrary({ ...opts, config, sitePath })
  const report = extractDependencies({
    ast,
    traverse,
    generate,
    filePath,
    depsImportAlias,
    depsFilePath,
    reexportLocalImportsViaBarrel: barrelOpts.reexportLocalImportsViaBarrel,
    barrelFilePath: barrelOpts.barrelFilePath,
    componentsImportPathForBarrel: barrelOpts.componentsImportPath,
  })

  const hasDepsPatch =
    report.depsPatch &&
    ((report.depsPatch.namespaceImports && report.depsPatch.namespaceImports.length > 0) ||
      (report.depsPatch.recordEntries && report.depsPatch.recordEntries.length > 0))
  const needsCastNormalize = programHasTSTypeCastExpression(ast, traverse)
  const hasBarrelWork = Boolean(
    report.useBarrelReexports && report.barrelReexportLines && report.barrelReexportLines.length > 0,
  )
  const hasCodeRewrite = report.moved.length > 0 || hasBarrelWork
  const hasRegistryOrRewrite = hasDepsPatch || hasCodeRewrite
  if (!hasRegistryOrRewrite && !needsCastNormalize) {
    return { code: source, applied: false, report, depsFilePath }
  }
  if (!hasCodeRewrite && !needsCastNormalize) {
    return { code: source, applied: false, report, depsFilePath }
  }

  normalizeTSTypeCastExpressionsForGenerator(ast, traverse)

  const out = generate(ast, {
    retainLines: false,
    comments: true,
    compact: false,
  }).code

  return { code: out, applied: true, report, depsFilePath }
}

module.exports = {
  analyzeLibraryDeps,
  printLibraryDepsHints,
  reportHasCmsDependencyWork,
  analysisHasCmsDepsWork,
  needsOptionalDepsFilePrompt,
  applyLibraryDepsRewrite,
  resolveBarrelOptsForLibrary,
}
