/**
 * cli:scaffold-routes — generate.ts
 *
 * Emits PageRegistry registrations matching SmartStack's dynamic routing
 * contract. The `componentKey` MUST be the dotted form derived from the route
 * by SmartStack (`/{app}/{module}/{section}` → `{app}.{module}.{section}`)
 * with optional sub-view suffix (`.create`, `.detail`, `.edit`) for non-list
 * views. Anything else (kebab keys, missing app prefix, missing section
 * segment) silently fails — DynamicRouter never resolves the page and the
 * user sees a perpetual spinner with no error in the console.
 *
 * Generated code imports from `@atlashub/smartstack` (npm package), not from
 * the SmartStack.app source tree. The page paths use the `@/pages/...` alias
 * configured in the client's `vite.config.ts` / `tsconfig.json`.
 *
 * See `GET /api/navigation/menu` for the authoritative key list.
 */
import type { ScaffoldRoutesInput, GeneratedFile } from './types.js'
import { validateComponentKey } from './types.js'

function kebabToCamel(s: string): string {
  return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase())
}

/** kebab → PascalCase (`module-home` → `ModuleHome`). Used to render the hub
 *  view names as valid TypeScript identifiers in the lazyWithRetry consts —
 *  the legacy `view.charAt(0).toUpperCase() + view.slice(1)` produced
 *  `Module-homePage` which is a parse error. */
function kebabToPascal(s: string): string {
  const camel = kebabToCamel(s)
  return camel.charAt(0).toUpperCase() + camel.slice(1)
}

export function generate(spec: ScaffoldRoutesInput): GeneratedFile[] {
  const files: GeneratedFile[] = []
  const registrations: string[] = []
  const lazyBindings: string[] = []
  // Dedup tracker (Bug E, 2026-05-27): both `create` and `edit` views map to
  // the same `${Entity}FormPage` const, so we must emit the lazyWithRetry
  // const exactly once per page name — even if both views appear in
  // `entity.views`. Registrations against different keys still happen for
  // each view, but they all reference the single shared const.
  const emittedConsts = new Set<string>()
  const appLower = spec.appCode.toLowerCase()
  const invalidKeys: string[] = []

  for (const entity of spec.entities) {
    // Sub-resource nesting (Bug D, 2026-05-27): when `parentSection` is set,
    // every key/path/URL gains an extra segment between the module and the
    // section. This mirrors how the SmartStack navigation DB stores 4-segment
    // keys for nested resources (`gaf.referentiels.types-affaire.types-audit`)
    // — without the parent segment, DynamicRouter couldn't resolve the page.
    const sectionKey = entity.parentSection
      ? `${appLower}.${spec.module}.${entity.parentSection}.${entity.section}`
      : `${appLower}.${spec.module}.${entity.section}`
    const basePath = entity.parentSection
      ? `@/pages/${appLower}/${spec.module}/${entity.parentSection}/${entity.section}`
      : `@/pages/${appLower}/${spec.module}/${entity.section}`
    // List pages use the plural entity name (BudgetsListPage.tsx) to match
    // scaffold-component; detail/form stay singular. When the BA passes an
    // explicit `pluralName` we use it verbatim (covers irregular plurals like
    // `Categories`/`Rues`); otherwise we fall back to the naive `${name}s`.
    const pluralName = entity.pluralName ?? `${entity.name}s`

    /**
     * Convert a project-relative file path (e.g. `src/pages/budgeting/budgets/
     * BudgetsListPage.tsx`) to the import path expected by Vite's `@` alias
     * (`@/pages/budgeting/budgets/BudgetsListPage`). Strips the leading `src/`
     * + the `.tsx` suffix.
     */
    const toImportPath = (relPath: string): string => {
      const normalised = relPath.replace(/\\/g, '/').replace(/\.tsx$/i, '')
      return normalised.startsWith('src/')
        ? `@/${normalised.slice(4)}`
        : `@/${normalised.replace(/^\.?\//, '')}`
    }

    for (const view of entity.views) {
      // The enum is exhaustive at the schema level; the final fallback covers
      // future view kinds and uses kebabToPascal so hyphenated hub views
      // (`module-home` → `ModuleHome`) emit valid TypeScript identifiers.
      const viewStr = view as string
      const pageName = view === 'list' ? `${pluralName}ListPage`
        : view === 'detail' ? `${entity.name}DetailPage`
        : view === 'create' || view === 'edit' ? `${entity.name}FormPage`
        : `${entity.name}${kebabToPascal(viewStr)}Page`

      // pageFilePaths override (from pageSpec.filePath) wins over the derived
      // basePath. This lets the registry import from the project's actual
      // page location (e.g. `pages/budgeting/...`) without forcing a folder
      // rename when scaffold-component's default convention (`pages/{module}/...`)
      // doesn't match the legacy layout.
      const overridePath = entity.pageFilePaths?.[view]
      const importPath = overridePath
        ? toImportPath(overridePath)
        : `${basePath}/${pageName}`
      // Update pageName when the override file name differs from the default
      // (e.g. legacy `BudgetsHomePage.tsx` instead of `BudgetsListPage.tsx`)
      // so the registry export resolution still finds a named export.
      const overrideBaseName = overridePath
        ? overridePath.replace(/\\/g, '/').replace(/^.*\//, '').replace(/\.tsx$/i, '')
        : pageName
      const effectiveName = overridePath ? overrideBaseName : pageName

      // Hub views resolve to special-shaped keys (Bug F, 2026-05-27):
      //   `module-home`  →  `{app}.{module}`              (the module root)
      //   `section-home` →  `{app}.{module}.{section}`    (the section root — same as `list`)
      // Without these special cases, `module-home` produced the redundant
      // 4-segment `{app}.{module}.module-home.module-home` (when the entity
      // section was also called `module-home`) which DynamicRouter never matches.
      // `app-home` is left at its default for now — surface a follow-up bug
      // if it bites.
      const key = view === 'module-home'
        ? `${appLower}.${spec.module}`
        : view === 'list' || view === 'section-home'
          ? sectionKey
          : `${sectionKey}.${view}`

      // Fail fast: invalid componentKey is unrecoverable at runtime (silent
      // spinner) so we refuse to emit the registry.
      const validationError = validateComponentKey(key)
      if (validationError) {
        invalidKeys.push(validationError)
        continue
      }

      // Wrap each lazyWithRetry() in `.then((m) => ({ default: m.X ?? m.default }))`
      // so the import works whether the page is exported as `default` or as
      // a named export matching `pageName`. Generated SmartStack pages use
      // named exports; bare `lazyWithRetry(() => import(...))` resolves to
      // `undefined` for those and mounts an empty Suspense fallback —
      // invisible spinner forever. We also throw at resolution time if
      // neither export exists, so the failure surfaces as a developer-readable
      // error instead of a silent spinner.
      //
      // `lazyWithRetry` (vs bare `React.lazy`) wraps the dynamic import in an
      // exponential-backoff retry. Transient `Failed to fetch dynamically
      // imported module` errors (HMR rebundle window, CDN flap, stale chunk
      // after deploy) resolve on retry instead of surfacing as a RouteErrorBoundary.
      //
      // Bug E (2026-05-27): we emit the const at most once per page name —
      // both `create` and `edit` views share `${Entity}FormPage`, and TS
      // refuses to compile a module with duplicate `const` declarations.
      if (!emittedConsts.has(effectiveName)) {
        emittedConsts.add(effectiveName)
        // Explicit `<unknown>` matches the generic constraint of
        // `lazyWithRetry<T extends ComponentType<unknown>>` in
        // @atlashub/smartstack. The bare `React.ComponentType` defaults to
        // `ComponentType<{}>`, which fails the constraint and Vite/TS
        // refuses the assignment (`Argument of type '() => Promise<{ default
        // … }>' is not assignable to parameter of type … ComponentType<unknown>`).
        lazyBindings.push(
          `const ${effectiveName} = lazyWithRetry(() =>
  import('${importPath}').then((m) => {
    const mod = m as { default?: React.ComponentType<unknown>; ${effectiveName}?: React.ComponentType<unknown> };
    const resolved = mod.${effectiveName} ?? mod.default;
    if (!resolved) {
      throw new Error("Page '${effectiveName}' at ${importPath} has no default or named export matching its file name.");
    }
    return { default: resolved };
  })
);`
        )
      }
      registrations.push(`PageRegistry.register('${key}', ${effectiveName});`)
    }
  }

  if (invalidKeys.length > 0) {
    throw new Error(
      `scaffold-routes: refusing to emit ${invalidKeys.length} invalid componentKey(s) — fix the spec first:\n  - ${invalidKeys.join('\n  - ')}`
    )
  }

  files.push({
    path: `src/extensions/${spec.module}Registry.ts`,
    content: `import React from 'react';
import { PageRegistry, lazyWithRetry } from '@atlashub/smartstack';

// ============================================================================
// ${spec.module.toUpperCase()} MODULE — Auto-generated route registrations
// ============================================================================
//
// Generated by skills/development/frontend/routes/cli/scaffold-routes.
// Do NOT edit by hand — re-run the CLI to regenerate.
//
// componentKey contract: dotted form ({app}.{module}.{section}[.{view}]),
// must equal the value returned by GET /api/navigation/menu for the matching
// route. Mismatch = silent spinner (DynamicRouter cannot resolve the page).
//
// This file MUST be imported from src/main.tsx BEFORE <App/> renders so the
// registrations run before the first route resolution. Import example:
//   import './extensions/${spec.module}Registry';

${lazyBindings.join('\n\n')}

${registrations.join('\n')}
`,
  })

  // Emit a per-module routes helper alongside the registry. Generated pages
  // import from this file rather than hardcoding `navigate('/foo/bar/new')`,
  // so URL paths and registry componentKeys stay in lockstep. Audit
  // DEV-UI-013 catches any page that bypasses it (hardcoded absolute URL in
  // navigate()).
  //
  // URL convention — must match the Core Navigation Seed which registers
  // routes as /{app}/{module}/{section} (3-seg). Without the app prefix
  // the URL helpers don't resolve to the menu entries seeded in DB and
  // DynamicRouter falls back to misinterpreting (e.g. /budgets/budgets/budgets
  // gets routed to /budgets/budgets/:id with id="budgets" → "introuvable"):
  //   list   → /{app}/{module}/{section}                  (componentKey = section root)
  //   detail → /{app}/{module}/{section}/${id}            (.detail suffix)
  //   edit   → /{app}/{module}/{section}/${id}/edit       (.edit suffix)
  //   create → /{app}/{module}/{section}/create           (.create suffix — NOT /new)
  // Pages MUST navigate via these helpers — hardcoding `/new` or `./new/edit`
  // produces a route DynamicRouter cannot resolve (silent spinner). Audit
  // DEV-UI-013 catches the drift.
  const sectionEntries = spec.entities
    .map((entity) => {
      const views = new Set(entity.views)
      const lines: string[] = []
      // Sub-resource URLs mirror the componentKey nesting: a sub-resource
      // under `types-affaire` reads `/gaf/referentiels/types-affaire/types-audit`,
      // not the flat `/gaf/referentiels/types-audit`. Without the parent
      // segment, the Core Navigation Seed has no matching menu entry and the
      // detail/edit pages 404.
      const urlBase = entity.parentSection
        ? `/${appLower}/${spec.module}/${entity.parentSection}/${entity.section}`
        : `/${appLower}/${spec.module}/${entity.section}`
      lines.push(`  ${kebabToCamel(entity.section)}: {`)
      lines.push(`    list: () => '${urlBase}',`)
      if (views.has('detail')) lines.push(`    detail: (id: string) => \`${urlBase}/\${id}\`,`)
      if (views.has('edit')) lines.push(`    edit: (id: string) => \`${urlBase}/\${id}/edit\`,`)
      if (views.has('create')) lines.push(`    create: () => '${urlBase}/create',`)
      if (views.has('dashboard')) lines.push(`    dashboard: () => '${urlBase}/dashboard',`)
      if (views.has('kanban')) lines.push(`    kanban: () => '${urlBase}/kanban',`)
      if (views.has('reconduction')) lines.push(`    reconduction: () => '${urlBase}/reconduction',`)
      lines.push(`  },`)
      return lines.join('\n')
    })
    .join('\n')

  files.push({
    path: `src/extensions/${spec.module}Routes.ts`,
    content: `// ============================================================================
// ${spec.module.toUpperCase()} MODULE — Auto-generated URL helpers
// ============================================================================
//
// Single source of truth for URL paths in the ${spec.module} module. Generated
// alongside ${spec.module}Registry.ts by skills/development/frontend/routes/cli/scaffold-routes.
// Do NOT edit by hand — re-run the CLI to regenerate.
//
// Generated pages MUST consume these helpers instead of hardcoding URLs in
// navigate() calls. Audit DEV-UI-013 rejects any *Page.tsx that uses an
// absolute string literal in navigate(...).
//
// Usage example:
//   import { routes } from '@/extensions/${spec.module}Routes';
//   navigate(routes.budgets.detail(item.id));
//   navigate(routes.budgets.create());

export const routes = {
${sectionEntries}
} as const;
`,
  })

  return files
}
