/**
 * cli:scaffold-component — types.ts
 */
import { z } from 'zod'

export const ComponentFieldSchema = z.object({
  name: z.string().min(1),
  type: z.string().min(1),
  required: z.boolean().default(true),
  label: z.string().optional(),
  /**
   * Optional flag (mirrored from the BA's `formula` on the data-model attribute):
   * marks the field as a **computed** value computed by the backend's LINQ
   * projection. Pages treat it as read-only :
   * - List columns render the value directly from `item.<name>` (the DTO already
   *   has it — projected by scaffold-business). No stub helper, no `return 0`.
   * - Detail page renders the value as a `<dd>` like any other field.
   * - Form page **excludes** the field — it is never editable. No `<input>`,
   *   no entry in `${E}FormData`, no entry in the initial form object.
   * Audit DEV-UI-014 flags any list page emitting a stub helper for these
   * fields ; audit DEV-UI-016 flags raw `<table>` use. The flag itself is
   * boolean-ish (any truthy string) — the formula content is not consumed
   * here (it lives in scaffold-business' LINQ projection).
   */
  formula: z.string().optional(),
  /** Default value for the field (e.g. `true` for EstActif/IsActive booleans).
   *  When present, scaffold-component uses it in the initial form data object
   *  instead of the type-based fallback (`false` for bool, `''` for string). */
  defaultValue: z.unknown().optional(),
  /**
   * Marks the field as a FK to another entity. When present, scaffold-component
   * emits `<EntityLookup apiEndpoint="/api/{module}/{plural-kebab}/lookup" />`
   * (theme-compliant combobox scaffolded by scaffold-ui-primitives) instead of
   * a raw `<input type="text">`. The field's TS type becomes `string | null`
   * (the FK Guid) and Detail pages render the resolved `displayName` via
   * `use{Target}Lookup` rather than the Guid.
   *
   * Populated by `ba-develop` Phase 3a from `entité.md` Relations: lines
   * (e.g. `Employee *→1 Department (FK DepartmentId, restrict)` →
   * `{ entity: 'Department', module: 'hr' }`). When unresolved (FK has no
   * declared relation), the orchestrator emits a warning and the field falls
   * back to `<input>` — audit DEV-UI-022 then surfaces it as BLOCKING.
   *
   * `apiEndpoint` is computed from `module` + plural-kebab(entity) when
   * omitted; provide it explicitly only when the route diverges from the
   * convention (e.g. Core entities under `/api/core/...`).
   */
  fkTo: z.object({
    entity: z.string().regex(/^[A-Z][A-Za-z0-9]*$/),
    module: z.string().min(1),
    apiEndpoint: z.string().optional(),
  }).optional(),
  /**
   * Fixed-list choices for an enum / status field. When present (and the field
   * is NOT a FK), scaffold-component emits `<EnumSelect>` (single) or
   * `<MultiSelect>` (when `multiple` is true) — both theme-compliant primitives
   * from scaffold-ui-primitives — instead of a raw `<input type="text">`.
   *
   * Populated by `ba-develop` Phase 3a from the data-model enumerations in
   * `entité.md` (analogous to how it resolves `fkTo` from Relations). Each
   * option's `value` is the persisted code; `label` is the display text.
   */
  options: z.array(z.object({
    value: z.string().min(1),
    label: z.string().min(1),
  }).passthrough()).optional(),
  /**
   * Marks an enum field (see `options`) as multi-valued → rendered as
   * `<MultiSelect>` and typed `string[]` in `${E}FormData` (initial value `[]`).
   * Ignored when `options` is absent.
   */
  multiple: z.boolean().optional(),
})

/**
 * Per-page spec passed in enriched mode (Phase 3a fetches it from the
 * sidecar via GET /prds/:id/page/:entity/:view). Validation already happened
 * at the IPC boundary against the canonical PageSpecSchema in
 * `src/backend/application/validators/persist-prd/types.ts` — we keep the
 * structural Zod check here loose (passthrough) and only enforce the few
 * fields the generator actually reads. Drift between the two definitions is
 * caught by the contract test in __tests__/page-spec-contract.test.ts.
 *
 * IMPORTANT: Skill code cannot import from `src/backend/...` because it ships
 * standalone to client apps; that's why we re-declare a minimal subset here
 * instead of re-using the canonical schema.
 */
const PageActionMinSchema = z.object({
  code: z.string().min(1),
  scope: z.enum(['header', 'row', 'bulk']),
  labelKey: z.string().min(1),
  permission: z.string().min(1),
  variant: z.enum(['primary', 'secondary', 'danger']).optional(),
}).passthrough()

const PageColumnMinSchema = z.object({
  key: z.string().min(1),
  labelKey: z.string().min(1),
  formatHint: z.string().optional(),
  sortable: z.boolean().optional(),
  filterable: z.boolean().optional(),
  searchable: z.boolean().optional(),
  isComputed: z.boolean().optional(),
  /**
   * Responsive visibility — minimum viewport breakpoint at which the column
   * appears in the list table (consumed by <ResponsiveDataTable>). Omit = always
   * visible. scaffold-component leaves the code + label columns (and the actions
   * column) always-visible and assigns md → lg → xl to the remaining columns by
   * declaration order; set this to override that default per column.
   */
  minBreakpoint: z.enum(['sm', 'md', 'lg', 'xl']).optional(),
  /**
   * Column priority. `'always'` forces the column to stay visible at every
   * breakpoint (same as omitting `minBreakpoint`). The other values are advisory
   * hints reserved for future width-budget logic.
   */
  priority: z.enum(['always', 'high', 'medium', 'low']).optional(),
  /**
   * When true (the scaffold default for text columns), the cell clips overflow
   * with an ellipsis and shows the full value in a tooltip on hover (handled by
   * <ResponsiveDataTable> via <TruncatedText>). Set false to opt a column out.
   */
  truncate: z.boolean().optional(),
}).passthrough()

/**
 * Filter declaration mined from `BaScreen.Config.filters[]` by create-prd.
 * The CLI renders ONE input per filter above the DataTable; the chosen
 * `control` decides the input shape. `defaultValue` seeds the local React
 * state on mount.
 */
const PageFilterMinSchema = z.object({
  field: z.string().min(1),
  labelKey: z.string().min(1),
  control: z.string().optional().default('text'),
  // Tolerate `select` options if Claude inlines them in the pageSpec.
  options: z.array(z.unknown()).optional(),
  defaultValue: z.unknown().optional(),
}).passthrough()

// NB: zod v4 (the deployed skill runtime) requires z.record(keyType, valueType)
// — the 1-arg form (v3-only) breaks under v4. The 2-arg form is valid in both.
const I18nMapSchema = z.record(z.string(), z.string())

/**
 * Widget on a home page (SmartAppHome / SmartModuleHome / SmartSectionHome).
 * Matches the BA create-screen `Config.widgets[]` vocabulary.
 */
const PageWidgetMinSchema = z.object({
  key: z.string().min(1),
  label: z.string().optional(),
  type: z.enum(['kpi', 'counter', 'chart-pie', 'chart-bar', 'chart-line', 'list']).optional(),
  entity: z.string().optional(),
  aggregation: z.string().optional(),
  field: z.string().optional(),
  col: z.number().optional(),
  labelKey: z.string().optional(),
  icon: z.string().optional(),
  /** Optional permission gating the widget (cross-module dashboards). */
  permission: z.string().optional(),
  /** Optional per-widget data source override (default: the dashboard endpoint). */
  endpoint: z.string().optional(),
}).passthrough()

/**
 * QuickLink on a home page — navigational card pointing to a child screen.
 */
const PageQuickLinkMinSchema = z.object({
  key: z.string().min(1),
  label: z.string().optional(),
  icon: z.string().optional(),
  screenTarget: z.string().optional(),
  permission: z.string().optional(),
  labelKey: z.string().optional(),
}).passthrough()

export const PageSpecMinSchema = z.object({
  screenCode: z.string().min(1),
  module: z.string().min(1),
  appCode: z.string().min(1),
  section: z.string().min(1),
  entity: z.string().min(1),
  view: z.string().min(1),
  filePath: z.string().min(1),
  permission: z.string().min(1),
  actions: z.array(PageActionMinSchema).default([]),
  columns: z.array(PageColumnMinSchema).optional(),
  filters: z.array(PageFilterMinSchema).optional(),
  /** KPI / counter / chart widgets for hub pages (app-home, module-home, section-home). */
  widgets: z.array(PageWidgetMinSchema).optional(),
  /** Navigation cards for hub pages — link to child screens. */
  quickLinks: z.array(PageQuickLinkMinSchema).optional(),
  /** Tab definitions for detail/form pages with tabbed layout.
   *  Each tab groups a subset of fields under a labelled panel. */
  tabs: z.array(z.object({
    key: z.string().min(1),
    label: z.string().optional(),
    labelKey: z.string().optional(),
    fields: z.array(z.string()).optional(),
  }).passthrough()).optional(),
  i18nKeys: z.object({
    fr: I18nMapSchema,
    en: I18nMapSchema,
    it: I18nMapSchema,
    de: I18nMapSchema,
  }).passthrough(),
  needsRefinement: z.boolean().default(false),
  specHash: z.string().min(1),
}).passthrough()

export const PrdContextMinSchema = z.object({
  appCode: z.string().min(1),
  i18nConfig: z.unknown().optional(),
  accessibility: z.unknown().optional(),
  security: z.unknown().optional(),
}).passthrough()

/**
 * Kanban view configuration. When `views` includes `'kanban'`, the generator
 * needs to know which field defines the columns (status / state / phase) and
 * what to display on each card. This config is optional; sensible defaults
 * (groupBy: 'status', columns from BA i18n keys) cover the common case.
 *
 * Drag-and-drop is opt-in (`dnd: true`) — most kanbans are read-only displays
 * of workflow state. When enabled, the generator emits handlers that call a
 * `useMove{E}` mutation hook (must exist in api-client output as a customAction
 * with `code: 'move'`).
 */
const KanbanConfigSchema = z.object({
  /** Field on the entity whose value defines the column. Must be a string-
   *  shaped field whose distinct values match the `columns[].key` list below. */
  groupBy: z.string().min(1).default('status'),
  /** Fixed-order column definitions. Drives the Kanban board left-to-right.
   *  `key` is the raw value of `groupBy`; `labelKey` is the i18n key on the
   *  page bundle (e.g. `kanban.columns.open`). */
  columns: z.array(z.object({
    key: z.string().min(1),
    labelKey: z.string().min(1),
  })).min(1),
  /** Field shown as the card title (default: the entity's first non-formula field). */
  titleField: z.string().optional(),
  /** Secondary line on the card (e.g. assignee, due date). */
  subtitleField: z.string().optional(),
  /** When true, cards can be dragged between columns. Requires a `move`
   *  customAction on the api-client side (calls `service.move(id, { newStatus })`). */
  dnd: z.boolean().default(false),
}).passthrough()

/**
 * Reconduction view configuration. Used for bulk re-issuing of records (e.g.
 * yearly renewal of contracts). Renders a filterable table of eligible records
 * with a multi-select column and a single "Reconduct selected" header button
 * that calls `useReconduct{Plural}` (api-client bulk action).
 *
 * `eligibilityFilter` (optional) is a label/key pair forwarded to the page as
 * a static filter chip — purely cosmetic, the real filtering happens
 * server-side via `useReconductable{Plural}` (api-client list call with a
 * `?eligibleForReconduction=true` query).
 */
const ReconductionConfigSchema = z.object({
  /** Header button labelKey (defaults to `reconduction.action.reconduct`). */
  actionLabelKey: z.string().min(1).default('reconduction.action.reconduct'),
  /** When true, the page also exposes a "Refuse / mark as not reconducting"
   *  bulk action — uses `useRefuseReconduction{Plural}` from api-client. */
  withRefuse: z.boolean().default(false),
}).passthrough()

export const ScaffoldComponentInputSchema = z.object({
  module: z.string().min(1),
  appCode: z.string().min(1),
  entity: z.string().min(1),
  section: z.string().min(1),
  views: z.array(z.enum([
    'list', 'detail', 'form', 'dashboard',
    // Hub views — landing pages for each scope level. Generated as
    // `${plural}AppHomePage.tsx` / `${plural}ModuleHomePage.tsx` /
    // `${plural}SectionHomePage.tsx` to match the existing SmartStack
    // naming convention (BudgetsModuleHomePage etc.).
    'app-home', 'module-home', 'section-home',
    // Bug 7 fix — kanban + reconduction are domain-recurring patterns
    // (AFFAIRES/Demande workflow, contract renewal) so we promote them to
    // first-class views with reusable primitives instead of relying on
    // hand-written pages that drift out of sync with the generated entities.
    'kanban', 'reconduction',
  ])).default(['list', 'detail', 'form']),
  /** Kanban-view configuration. REQUIRED when `views` includes `'kanban'`. */
  kanbanConfig: KanbanConfigSchema.optional(),
  /** Reconduction-view configuration. Optional when `views` includes `'reconduction'`. */
  reconductionConfig: ReconductionConfigSchema.optional(),
  fields: z.array(ComponentFieldSchema).default([]),
  projectPath: z.string().min(1),
  /**
   * Relative path from `projectPath` to the web project root (the folder
   * containing `src/`, `package.json`, `tsconfig.json`). Defaults to
   * `web/${appCode}-web` for canonical SmartStack layout. Override when the
   * web folder has a different name (e.g. `web/custom-web` when appCode is
   * not the default). i18n locale files are written inside this folder.
   */
  webRoot: z.string().optional(),
  /**
   * Diagnostics from a previous validate-page run on the same target file.
   * Forwarded by the per-page orchestrator on the retry pass so any future
   * Claude-assisted template can adapt. Today's templates are pure-functional
   * and ignore the field; presence is a no-op beyond enabling --overwrite
   * downstream.
   */
  priorErrors: z.array(z.string()).optional(),
  /**
   * Enriched mode (Phase 3a, post-PRD-pageSpec). When present, the generator
   * uses pageSpec.i18nKeys verbatim (instead of inferring), pageSpec.actions
   * for button permissions (instead of hardcoded {section}.create/.update/
   * .delete heuristics), and pageSpec.columns for column metadata (sortable,
   * filterable, searchable). Legacy fields above must STILL be provided —
   * Phase 3a derives them from the pageSpec when calling this CLI. That way
   * the existing generator pipeline runs unchanged in legacy mode and only
   * the data-binding step branches on pageSpec presence.
   */
  pageSpec: PageSpecMinSchema.optional(),
  prdContext: PrdContextMinSchema.optional(),
})

export type ComponentField = z.infer<typeof ComponentFieldSchema>
export type ScaffoldComponentInput = z.infer<typeof ScaffoldComponentInputSchema>
export type PageSpecMin = z.infer<typeof PageSpecMinSchema>
export type PrdContextMin = z.infer<typeof PrdContextMinSchema>
export type KanbanConfig = z.infer<typeof KanbanConfigSchema>
export type ReconductionConfig = z.infer<typeof ReconductionConfigSchema>
export interface GeneratedFile {
  path: string
  content: string
}
export interface ValidationResult {
  valid: boolean
  errors: string[]
  warnings: string[]
}
