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

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

const { normalizeTSTypeCastExpressionsForGenerator } = require('./codemod/utils')

/**
 * Build a stable PascalCase identifier from an npm package specifier (e.g. lucide-react → LucideReact).
 */
function packageToNamespaceIdentifier(spec) {
  let rest = spec
  if (rest.startsWith('@')) {
    const slash = rest.indexOf('/', 1)
    rest = slash >= 0 ? rest.slice(slash + 1) : rest.slice(1)
  }
  return rest
    .split('/')
    .flatMap((seg) => seg.split('-'))
    .filter(Boolean)
    .map((p) => p.charAt(0).toUpperCase() + p.slice(1))
    .join('') || 'Pkg'
}

function isTopLevelVariableDeclaratorPath(p) {
  const vd = p.parentPath
  if (!vd.isVariableDeclaration()) return false
  const gp = vd.parentPath
  return gp.isProgram() || gp.isExportNamedDeclaration()
}

/**
 * @param {string} depsFilePath
 * @returns {{ namespaces: Map<string, string>, recordKeys: Set<string> }}
 */
function analyzeExistingCmsDepsFile(depsFilePath) {
  const empty = { namespaces: new Map(), recordKeys: new Set() }
  if (!depsFilePath || !fs.existsSync(depsFilePath)) return empty
  let ast
  try {
    ast = parser.parse(fs.readFileSync(depsFilePath, 'utf8'), {
      sourceType: 'module',
      plugins: ['typescript', 'jsx'],
      errorRecovery: true,
    })
  } catch {
    return empty
  }

  const namespaces = new Map()
  const recordKeys = new Set()

  traverse(ast, {
    ImportDeclaration(p) {
      const src = p.node.source.value
      for (const s of p.node.specifiers) {
        if (t.isImportNamespaceSpecifier(s)) {
          namespaces.set(src, s.local.name)
        }
      }
    },
    VariableDeclarator(p) {
      if (!isTopLevelVariableDeclaratorPath(p)) return
      const id = p.node.id
      if (!t.isIdentifier(id) || id.name !== 'cmsDependencies') return
      if (!t.isObjectExpression(p.node.init)) return
      for (const prop of p.node.init.properties) {
        if (!t.isObjectProperty(prop)) continue
        const k = prop.key
        if (t.isStringLiteral(k)) recordKeys.add(k.value)
        else if (t.isIdentifier(k)) recordKeys.add(k.name)
      }
    },
  })

  return { namespaces, recordKeys }
}

function recordKeyNode(spec) {
  if (/^[a-zA-Z_$][\w$]*$/.test(spec)) {
    return t.identifier(spec)
  }
  return t.stringLiteral(spec)
}

function findCmsDependenciesObjectPath(ast) {
  let found = null
  traverse(ast, {
    VariableDeclarator(p) {
      if (!isTopLevelVariableDeclaratorPath(p)) return
      if (!t.isIdentifier(p.node.id) || p.node.id.name !== 'cmsDependencies') return
      if (!t.isObjectExpression(p.node.init)) return
      found = p.get('init')
      p.stop()
    },
  })
  return found
}

function lastImportInsertIndex(body) {
  let i = 0
  while (i < body.length && t.isImportDeclaration(body[i])) i++
  return i
}

/**
 * Merge namespace imports and cmsDependencies record entries into the consumer deps file.
 *
 * @param {string} depsFilePath
 * @param {{ namespaceImports: { spec: string, namespaceId: string }[], recordEntries: { spec: string, namespaceId: string }[] }} patch
 */
async function patchCmsDependenciesFile(depsFilePath, patch) {
  if (!depsFilePath || !patch) return
  const importList = patch.namespaceImports || []
  const entryList = patch.recordEntries || []
  if (!importList.length && !entryList.length) return

  const text = fs.existsSync(depsFilePath) ? fs.readFileSync(depsFilePath, 'utf8') : ''
  let ast
  try {
    ast = parser.parse(text || '', {
      sourceType: 'module',
      plugins: ['typescript', 'jsx'],
      errorRecovery: true,
    })
  } catch {
    ast = parser.parse('', { sourceType: 'module', plugins: ['typescript', 'jsx'] })
  }

  const { namespaces, recordKeys } = analyzeExistingCmsDepsFile(depsFilePath)

  let programPath
  traverse(ast, {
    Program(p) {
      programPath = p
    },
  })
  if (!programPath) return

  const body = programPath.node.body

  let insertAt = lastImportInsertIndex(body)
  const seenImportSpec = new Set()
  for (const { spec, namespaceId } of importList) {
    if (seenImportSpec.has(spec)) continue
    seenImportSpec.add(spec)
    if (recordKeys.has(spec)) continue
    if (namespaces.has(spec)) continue
    const decl = t.importDeclaration(
      [t.importNamespaceSpecifier(t.identifier(namespaceId))],
      t.stringLiteral(spec),
    )
    body.splice(insertAt, 0, decl)
    insertAt++
    namespaces.set(spec, namespaceId)
  }

  const objPath = findCmsDependenciesObjectPath(ast)
  const seenEntrySpec = new Set()

  if (objPath) {
    const node = objPath.node
    for (const { spec, namespaceId } of entryList) {
      if (seenEntrySpec.has(spec)) continue
      seenEntrySpec.add(spec)
      if (recordKeys.has(spec)) continue
      node.properties.push(
        t.objectProperty(
          recordKeyNode(spec),
          t.objectExpression([
            t.objectProperty(t.identifier('module'), t.identifier(namespaceId)),
          ]),
        ),
      )
      recordKeys.add(spec)
    }
  } else {
    const props = []
    for (const { spec, namespaceId } of entryList) {
      if (seenEntrySpec.has(spec)) continue
      seenEntrySpec.add(spec)
      if (recordKeys.has(spec)) continue
      props.push(
        t.objectProperty(
          recordKeyNode(spec),
          t.objectExpression([
            t.objectProperty(t.identifier('module'), t.identifier(namespaceId)),
          ]),
        ),
      )
      recordKeys.add(spec)
    }
    if (props.length) {
      const decl = t.variableDeclaration('const', [
        t.variableDeclarator(t.identifier('cmsDependencies'), t.objectExpression(props)),
      ])
      body.splice(insertAt, 0, t.exportNamedDeclaration(decl))
    }
  }

  normalizeTSTypeCastExpressionsForGenerator(ast, traverse)

  let out = generate(ast, { retainLines: false, comments: true, compact: false }).code
  out = await formatPatchedDepsWithPrettier(depsFilePath, out)
  fs.writeFileSync(depsFilePath, out + (out.endsWith('\n') ? '' : '\n'), 'utf8')
}

function formatDepsPatchForDisplay(patch) {
  if (!patch) return ''
  const im = patch.namespaceImports || []
  const en = patch.recordEntries || []
  if (!im.length && !en.length) return ''
  const lines = []
  for (let i = 0; i < im.length; i++) {
    const { spec, namespaceId } = im[i]
    lines.push(`import * as ${namespaceId} from '${spec}'`)
  }
  lines.push('')
  lines.push('// In cmsDependencies:')
  for (let i = 0; i < en.length; i++) {
    const { spec, namespaceId } = en[i]
    const comma = i < en.length - 1 ? ',' : ''
    lines.push(`  '${spec}': { module: ${namespaceId} }${comma}`)
  }
  return lines.join('\n')
}

async function formatPatchedDepsWithPrettier(depsFilePath, code) {
  const ext = path.extname(depsFilePath || '').toLowerCase()
  if (ext !== '.ts' && ext !== '.tsx') return code
  try {
    const prettier = require('prettier')
    const config = (await prettier.resolveConfig(depsFilePath)) || {}
    return await prettier.format(code, { ...config, filepath: depsFilePath })
  } catch {
    return code
  }
}

module.exports = {
  packageToNamespaceIdentifier,
  analyzeExistingCmsDepsFile,
  patchCmsDependenciesFile,
  formatDepsPatchForDisplay,
}
