/**
 * cli:aggregate-component-registry — generate.ts
 *
 * Glob → parse → verify → emit.
 *
 * 1. Glob `src/extensions/*Registry.ts` (excluding the generated aggregator).
 * 2. Parse each file: extract `PageRegistry.register('<key>', <pageVar>)` calls
 *    and the `import('<path>')` literals from the matching `lazy(...)` or
 *    `lazyWithRetry(...)` blocks (both are tracked — see audit.ts regex).
 *    Regexes mirror those in audit-dev-frontend's audit.ts (single source).
 * 3. Verify every `import(<path>)` resolves on disk under the project's `@/`
 *    alias (= `src/`). Phantom imports = hard error (Vite fails at launch).
 * 4. Verify no two modules registered the same `<key>` — silent collision is
 *    impossible to debug at runtime (last registration wins).
 * 5. Emit a consolidated `src/extensions/componentRegistry.generated.ts`
 *    that re-exports every per-module `*Registry` via a side-effect import.
 *    Bun-style ordering: alphabetical, single source of truth for `main.tsx`.
 */
import fs from 'node:fs'
import path from 'node:path'
import {
  type AggregateComponentRegistryInput,
  type GeneratedFile,
  type ParsedRegistry,
  type ResolvedImport,
} from './types.js'

const REGISTER_RE = /PageRegistry\.register\(\s*['"`]([^'"`]+)['"`]\s*,\s*([A-Z][A-Za-z0-9_]*)\s*\)/g
const LAZY_IMPORT_RE = /import\(\s*['"`]([^'"`]+)['"`]\s*\)/g
const REGISTRY_FILE_RE = /^([a-zA-Z][a-zA-Z0-9_-]*)Registry\.ts$/

/**
 * Bare module-level componentKeys returned by /api/navigation/menu for
 * SmartStack platform admin modules. The package's PAGE_KEYS only declares
 * leaf keys (USERS_LIST etc.), so without explicit fallback registrations
 * DynamicRouter throws MISSING_PAGE on every navigation to a bare admin URL.
 *
 * This block is prepended to every generated componentRegistry so generated
 * apps don't have to know about this upstream gap. Per-module registries
 * registered AFTER this block can override any of these keys (last-write-wins
 * in PageRegistry).
 *
 * Source: keys observed in NavigationService output for the smartstack
 * platform NuGet package's admin controllers ([NavRoute("administration.X")]).
 */
const PLATFORM_ADMIN_BARE_KEYS = [
  'administration',
  'administration.users',
  'administration.permissions',
  'administration.applications',
  'administration.ai',
  'administration.workflows',
  'administration.entra',
  'administration.tenants',
  'administration.configuration',
  'administration.uiconfiguration',
] as const

const PLATFORM_ADMIN_FALLBACK_BLOCK = `// ─── Platform admin fallbacks (auto-generated) ─────────────────────────────
// SmartStack platform admin modules are returned by /api/navigation/menu but
// the package's PageRegistry only declares leaf keys (USERS_LIST etc.).
// Without these fallback registrations DynamicRouter throws MISSING_PAGE on
// every /administration/<module> URL. Per-module registries imported below
// can override any of these keys.
import { PageRegistry, lazyWithRetry } from '@atlashub/smartstack';

const AdminModuleFallback = lazyWithRetry(() =>
  import('@/components/AdminModuleFallback').then((m) => ({
    default: m.AdminModuleFallback,
  }))
);

${PLATFORM_ADMIN_BARE_KEYS.map((k) => `PageRegistry.register('${k}', AdminModuleFallback);`).join('\n')}
`

export interface GenerateResult {
  files: GeneratedFile[]
  parsed: ParsedRegistry[]
  unresolved: ResolvedImport[]
  collisions: Array<{ key: string; modules: string[] }>
  warnings: string[]
}

export function generate(spec: AggregateComponentRegistryInput): GenerateResult {
  const extensionsDir = path.join(spec.projectPath, 'src', 'extensions')
  const outFile = spec.outFile ?? 'src/extensions/componentRegistry.generated.ts'
  const outAbs = path.join(spec.projectPath, outFile)
  const outBasename = path.basename(outAbs)

  // 1. Discover registry files
  const allFiles = fs.existsSync(extensionsDir)
    ? fs.readdirSync(extensionsDir).filter((f) => f.endsWith('.ts'))
    : []

  const excludeSet = new Set([outBasename, ...spec.exclude])
  const candidates = allFiles
    .filter((f) => !excludeSet.has(f))
    .filter((f) => REGISTRY_FILE_RE.test(f))

  let registryFiles: string[]
  const warnings: string[] = []
  if (spec.modules) {
    const wanted = new Set(spec.modules.map((m) => `${m}Registry.ts`))
    registryFiles = Array.from(wanted).filter((f) => candidates.includes(f))
    const missing = Array.from(wanted).filter((f) => !candidates.includes(f))
    for (const m of missing) {
      warnings.push(`Requested module registry not found on disk: ${m}`)
    }
    const orphans = candidates.filter((f) => !wanted.has(f))
    for (const o of orphans) {
      warnings.push(
        `Found ${o} on disk but it was not in --modules — likely orphan from a deleted module. Either re-include it or remove the file.`,
      )
    }
  } else {
    registryFiles = candidates
  }
  registryFiles.sort()

  // 2. Parse each registry
  const parsed: ParsedRegistry[] = registryFiles.map((file) => {
    const abs = path.join(extensionsDir, file)
    const content = fs.readFileSync(abs, 'utf-8')
    const moduleName = file.replace(/Registry\.ts$/, '')
    const keys: string[] = []
    const lazyImports: string[] = []
    REGISTER_RE.lastIndex = 0
    let m: RegExpExecArray | null
    while ((m = REGISTER_RE.exec(content)) !== null) keys.push(m[1])
    LAZY_IMPORT_RE.lastIndex = 0
    while ((m = LAZY_IMPORT_RE.exec(content)) !== null) lazyImports.push(m[1])
    return { file, module: moduleName, keys, lazyImports }
  })

  // 3. Resolve every lazy import
  const unresolved: ResolvedImport[] = []
  for (const reg of parsed) {
    for (const importPath of reg.lazyImports) {
      const resolved = resolveLazyImport(importPath, spec.projectPath)
      if (!resolved) {
        unresolved.push({ module: reg.module, importPath, resolved: null })
      }
    }
  }

  // 4. Detect key collisions across modules
  const keyOwners = new Map<string, string[]>()
  for (const reg of parsed) {
    for (const k of reg.keys) {
      const owners = keyOwners.get(k) ?? []
      owners.push(reg.module)
      keyOwners.set(k, owners)
    }
  }
  const collisions = Array.from(keyOwners.entries())
    .filter(([, owners]) => owners.length > 1)
    .map(([key, modules]) => ({ key, modules }))

  // 5. Emit consolidated file (only if no errors — caller checks unresolved + collisions)
  const files: GeneratedFile[] = []
  if (unresolved.length === 0 && collisions.length === 0) {
    const lines = parsed.map((p) => `import './${p.module}Registry';`)
    const content = `// ============================================================================
// componentRegistry.generated.ts — Auto-aggregated module registrations
// ============================================================================
//
// Generated by skills/development/frontend/routes/cli/aggregate-component-registry.
// Do NOT edit by hand — re-run the CLI to regenerate.
//
// Each side-effect import below pulls in one module's PageRegistry.register()
// calls. This file MUST be imported from src/main.tsx BEFORE <App/> renders so
// the registry is populated before the first DynamicRouter resolution.
//
//   import './extensions/componentRegistry.generated';
//
// Modules aggregated: ${parsed.length}
// Total componentKeys: ${parsed.reduce((acc, p) => acc + p.keys.length, 0)}

${PLATFORM_ADMIN_FALLBACK_BLOCK}
${lines.join('\n')}
`
    files.push({ path: outFile, content })
  }

  return { files, parsed, unresolved, collisions, warnings }
}

/**
 * Resolve a lazy `import('<path>')` argument against the project's `@/` alias
 * (= `src/`). Tries `.tsx`, `.ts`, `index.tsx`, `index.ts`. Returns the
 * absolute filesystem path of the resolved file, or `null` when nothing
 * matches — that's a phantom import.
 */
export function resolveLazyImport(
  importPath: string,
  projectPath: string,
): string | null {
  // Normalize the alias '@/' → 'src/'. Other forms (relative './foo', absolute
  // 'foo/bar') are unusual in our generated registries — handle them anyway.
  const aliased = importPath.startsWith('@/')
    ? path.join(projectPath, 'src', importPath.slice(2))
    : importPath.startsWith('./') || importPath.startsWith('../')
      ? path.join(projectPath, 'src', 'extensions', importPath)
      : path.join(projectPath, importPath)

  const candidates = [
    `${aliased}.tsx`,
    `${aliased}.ts`,
    path.join(aliased, 'index.tsx'),
    path.join(aliased, 'index.ts'),
  ]
  for (const c of candidates) {
    if (fs.existsSync(c)) return c
  }
  return null
}
