/**
 * cli:scaffold-doc — generate.ts
 *
 * Deterministic file ops (i18n locale JSON + docs-manifest.json upsert, both
 * idempotent) and computation of the wiring plan for the in-place TSX edits.
 * Locates the wiring target files by glob so it adapts to client projects that
 * differ from the reference app; when a target is missing it still returns the
 * instruction (file=null) so nothing is silently skipped.
 */
import path from 'node:path'
import { findSmartStackStructure } from '../../../lib/detector.js'
import { findFiles, readJson, writeJson, fileExists } from '../../../lib/fs.js'
import { toKebabCase } from '../../../lib/string-utils.js'
import { LANGS, type ScaffoldDocInput, type WiringInstruction } from './types.js'

export interface GenerateResult {
  filesCreated: string[]
  filesModified: string[]
  warnings: string[]
  wiring: WiringInstruction[]
  nextSteps: string[]
}

export async function generate(spec: ScaffoldDocInput): Promise<GenerateResult> {
  const res: GenerateResult = { filesCreated: [], filesModified: [], warnings: [], wiring: [], nextSteps: [] }
  const structure = await findSmartStackStructure(spec.projectPath)
  const web = structure.web
  if (!web) {
    res.warnings.push('Web project not found under projectPath — cannot write i18n / manifest or locate wiring targets.')
    return res
  }

  const kebab = toKebabCase(spec.namespace) // docsAdministrationUsers → docs-administration-users
  const routeFull = `/docs/business/${spec.routePath}`

  // ── 1. i18n locale files (FLAT shape, canonical central location) ────────
  if (spec.i18n) {
    for (const lang of LANGS) {
      const content = spec.i18n[lang]
      if (!content) continue
      // user-type docs are FLAT (resolved via t('title')); DocRenderer types
      // (developer/database/testing, resolved via t('<namespace>.path')) nest the
      // content under the namespace key. Author flat content — the CLI wraps it.
      const payload = spec.type === 'user' ? content : { [spec.namespace]: content }
      const abs = path.join(web, 'src', 'i18n', 'locales', lang, `${kebab}.json`)
      const existed = await fileExists(abs)
      if (!spec.dryRun) await writeJson(abs, payload)
      ;(existed ? res.filesModified : res.filesCreated).push(relTo(spec.projectPath, abs))
    }
  }

  // ── 2. docs-manifest.json upsert (idempotent by id) ──────────────────────
  const manifestAbs = path.join(web, 'src', 'pages', 'docs', 'docs-manifest.json')
  if (await fileExists(manifestAbs)) {
    const manifest = await readJson<{ modules?: ManifestEntry[]; lastUpdated?: string; [k: string]: unknown }>(
      manifestAbs,
    )
    if (!Array.isArray(manifest.modules)) manifest.modules = []
    const now = new Date().toISOString()
    const languages = spec.i18n ? Object.keys(spec.i18n) : [...LANGS]
    const dataFile = `${spec.pageImportPath.replace(/^@\/pages\/docs\//, '')}/index.tsx`
    const existingIdx = manifest.modules.findIndex((m) => m.id === spec.manifest.id)
    const entry: ManifestEntry = {
      id: spec.manifest.id,
      type: spec.type,
      application: spec.application ?? '',
      module: spec.target,
      name: spec.manifest.name,
      description: spec.manifest.description,
      path: routeFull,
      dataFile,
      i18nNamespace: spec.namespace,
      version: spec.manifest.version,
      createdAt: existingIdx >= 0 ? (manifest.modules[existingIdx].createdAt ?? now) : now,
      updatedAt: now,
      languages,
      deferredLanguages: spec.manifest.deferredLanguages,
    }
    if (existingIdx >= 0) manifest.modules[existingIdx] = entry
    else manifest.modules.push(entry)
    manifest.lastUpdated = now
    if (!spec.dryRun) await writeJson(manifestAbs, manifest)
    res.filesModified.push(relTo(spec.projectPath, manifestAbs))
  } else {
    res.warnings.push(
      `docs-manifest.json not found at ${relTo(spec.projectPath, manifestAbs)} — manifest upsert skipped.`,
    )
  }

  // ── 3. Wiring plan (TSX in-place edits Claude applies with Edit) ─────────
  res.wiring = await computeWiring(spec, web, kebab, routeFull)

  res.nextSteps = [
    spec.i18n
      ? 'i18n files written. Author the doc page body per templates.md (faithful Mock UI for user type).'
      : 'Author the doc page body + the 4 i18n locale JSON files (FLAT) per templates.md.',
    `Apply the ${res.wiring.length} wiring instruction(s) in report.data.wiring with Edit — each has an idempotencyMarker; skip if already present in the file.`,
    'Run /ui-components (ui-polish CLI) on the generated page to guarantee zero hardcoded colors.',
  ]
  return res
}

interface ManifestEntry {
  id: string
  type: string
  application: string
  module: string
  name: string
  description: string
  path: string
  dataFile: string
  i18nNamespace: string
  version: string
  createdAt?: string
  updatedAt?: string
  languages: string[]
  deferredLanguages: string[]
}

async function computeWiring(
  spec: ScaffoldDocInput,
  web: string,
  kebab: string,
  routeFull: string,
): Promise<WiringInstruction[]> {
  const wiring: WiringInstruction[] = []

  // (a) i18n config.ts — register the namespace (4 insertion points).
  const configAbs = (await findFirst(web, '**/i18n/config.ts')) ?? (await findFirst(web, '**/i18n/config.tsx'))
  wiring.push({
    file: configAbs ? relTo(spec.projectPath, configAbs) : null,
    action: `Register i18n namespace "${spec.namespace}" — FOUR insertion points in config.ts.`,
    anchor: `ns: [`,
    insert:
      `1) destructured var: add \`${spec.namespace}\` to the \`const [ … ] = await Promise.all([\` list.\n` +
      `2) import() call (same order): \`import(\`./locales/\${lang}/${kebab}.json\`).catch(() => ({ default: {} })),\`\n` +
      `3) return object: \`${spec.namespace}: ${spec.namespace}.default || {},\`\n` +
      `4) ns array: add the string \`'${spec.namespace}'\` inside \`ns: [ … ]\`.`,
    idempotencyMarker: `'${spec.namespace}'`,
  })

  // (b) DocRoutes.tsx — lazy import + child route under /docs/business.
  const routerAbs =
    (await findFirst(web, '**/DocRoutes.tsx')) ?? (await findFirst(web, '**/DynamicRouter.tsx'))
  wiring.push({
    file: routerAbs ? relTo(spec.projectPath, routerAbs) : null,
    action: 'Add the lazy import + child route for the doc page (route group "/docs/business").',
    anchor: `path="/docs/business"`,
    insert:
      `// near the other lazy doc imports:\n` +
      `const ${spec.componentName} = lazy(() => import('${spec.pageImportPath}'));\n` +
      `// inside the <Route path="/docs/business" element={<DocsLayout/>}> group, as a child:\n` +
      `<Route path="${spec.routePath}" element={<Suspense fallback={<PageLoader />}><${spec.componentName} /></Suspense>} />`,
    idempotencyMarker: spec.componentName,
  })

  // (c) DocPanelContext.tsx — docMapping (+ optional appDocMapping default).
  const panelAbs = await findFirst(web, '**/DocPanelContext.tsx')
  const keys = spec.docMappings.length > 0 ? spec.docMappings : [spec.routePath]
  const mappingLines = keys.map((k) => `  '${k}': '${routeFull}',`).join('\n')
  wiring.push({
    file: panelAbs ? relTo(spec.projectPath, panelAbs) : null,
    action: 'Add the contextual-panel route mappings (and the app default if requested).',
    anchor: `const docMapping`,
    insert:
      `// inside docMapping:\n${mappingLines}` +
      (spec.appDocDefault && spec.application
        ? `\n// inside appDocMapping:\n  '${spec.application}': '${routeFull}',`
        : ''),
    idempotencyMarker: `'${keys[0]}':`,
  })

  // (d) UserIndexPage.tsx — module entry (user type only).
  if (spec.type === 'user') {
    const indexAbs = await findFirst(web, '**/UserIndexPage.tsx')
    const camel = spec.target.replace(/-([a-z0-9])/g, (_, c: string) => c.toUpperCase())
    wiring.push({
      file: indexAbs ? relTo(spec.projectPath, indexAbs) : null,
      action: `Add the module entry under the "${spec.application ?? '?'}" application's modules[].`,
      anchor: `modules: [`,
      insert:
        `{ name: t('user.modules.${camel}.name'), icon: FileText, href: '${routeFull}', description: t('user.modules.${camel}.description') },\n` +
        `// also add user.modules.${camel}.name/.description to the docs i18n (UserIndexPage namespace), and pick a real lucide icon instead of FileText.`,
      idempotencyMarker: `'${routeFull}'`,
    })
  }

  return wiring
}

async function findFirst(cwd: string, pattern: string): Promise<string | null> {
  const files = await findFiles(pattern, { cwd })
  return files.length > 0 ? files[0] : null
}

function relTo(root: string, abs: string): string {
  return path.relative(root, abs).replace(/\\/g, '/')
}
