/**
 * lib/page-spec-actions.ts — Canonical Zod schema for custom page actions.
 *
 * SINGLE SOURCE OF TRUTH for the contract `screen.md` → `pagespec.actions[]` →
 * scaffold-controller / scaffold-business / scaffold-api-client / scaffold-component.
 *
 * The CLI deterministic generators already accept their own ad-hoc shapes
 * (`ControllerCustomActionSchema`, `BusinessCustomActionSchema`,
 * `ApiCustomActionSchema`, `PageActionMinSchema`). This module unifies the
 * upstream contract: ba-create-prd writes a `PageCustomAction[]` into every
 * pagespec, ba-develop derives each generator's input from it, and the dev
 * audits cross-check the generated code against the same schema.
 *
 * The fundamental invariant:
 *   pageSpec.action.endpoint  ===  [HttpVerb("<endpoint>")] route attribute
 *                             ===  apiClient.<verb>('/api/.../<endpoint>')
 *
 * The same string is propagated VERBATIM in both directions. Frontend and
 * backend can never drift on a name, because there is only one name.
 *
 * @see business-analyse/create-prd/SKILL.md  (propagates screen.md → pagespec)
 * @see ba-develop/SKILL.md                      (derives the generator inputs)
 * @see development/backend/controller/cli/scaffold-controller/types.ts (downstream consumer)
 * @see development/frontend/component/cli/scaffold-component/types.ts  (downstream consumer)
 * @see development/audit-dev-api/SKILL.md       (DEV-API-010/011 — pageSpec ↔ controller)
 * @see development/audit-dev-frontend/SKILL.md  (DEV-UI-019/020/021 — pageSpec ↔ service.ts)
 */
import { z } from 'zod'
import { toKebabCase, toCamelCase, toPascalCase } from './string-utils.js'

/**
 * Action codes emitted by the canonical CRUD paths of every scaffolder.
 * Filters here are applied BEFORE iterating customActions so we never emit
 * a duplicate POST for an action already handled by the standard generator.
 *
 * Examples:
 * - `create` (header) → `POST /` from scaffold-controller's actions[] enum
 * - `edit` (row)      → navigate(routes.<entity>.edit(item.id)) from scaffold-component
 * - `delete` (row)    → `DELETE /{id:guid}` from scaffold-controller
 */
export const STANDARD_CRUD_CODES = new Set<string>([
  'create', 'read', 'list', 'detail',
  'edit', 'update', 'delete',
])

/**
 * Compute the default URL segment for an action code.
 * The pageSpec author can override by setting `endpoint` explicitly — useful
 * when the business code (e.g. `syncFromPce`) maps to a legacy backend route
 * (e.g. `sync-from-proconcept`).
 *
 *   defaultEndpoint('syncFromPce')   === 'sync-from-pce'
 *   defaultEndpoint('archive')       === 'archive'
 *   defaultEndpoint('analyzeImpact') === 'analyze-impact'
 */
export function defaultEndpoint(code: string): string {
  return toKebabCase(code)
}

/**
 * Derive the C# controller method name from a pageSpec endpoint.
 * Scaffold-controller appends `Async`, so this helper returns the bare PascalCase
 * — the suffix is the generator's responsibility.
 *
 *   controllerMethodNameFromEndpoint('sync-from-proconcept') === 'SyncFromProconcept'
 *   controllerMethodNameFromEndpoint('archive')              === 'Archive'
 */
export function controllerMethodNameFromEndpoint(endpoint: string): string {
  return toPascalCase(endpoint)
}

/**
 * Derive the TypeScript service-method name from a pageSpec endpoint.
 * Used by scaffold-api-client when emitting `<entity>Service.ts` members.
 *
 *   serviceMethodNameFromEndpoint('sync-from-proconcept') === 'syncFromProconcept'
 *   serviceMethodNameFromEndpoint('archive')              === 'archive'
 */
export function serviceMethodNameFromEndpoint(endpoint: string): string {
  return toCamelCase(endpoint)
}

/**
 * Optional state-machine metadata. Populated by ba-create-prd from any
 * linked business rule of type `workflow` whose flow lists a status
 * transition. Consumed by scaffold-business to emit the guard
 * (`if (entity.Status not in fromStatus) throw …`) and the assignment
 * (`entity.Status = toStatus`).
 */
export const WorkflowTransitionSchema = z.object({
  /** Allowed source statuses (raw values from the Status enum/lookup). */
  fromStatus: z.array(z.string().min(1)).min(1),
  /** Target status (raw value). */
  toStatus: z.string().min(1),
  /** Names of parameters the use case requires on the request body. */
  flowParameters: z.array(z.string().min(1)).default([]),
})

export type WorkflowTransition = z.infer<typeof WorkflowTransitionSchema>

/**
 * One custom page action — the full vertical-slice contract.
 *
 * `kind: "api"` actions generate a backend endpoint + a service method + a
 * React Query hook + a button. `kind: "navigate"` actions generate ONLY a
 * button that calls `navigate(targetRoute)` — no API call, no hook, no axios
 * (this is the fix for the legacy pattern `POST /{id}/open` → 405).
 */
export const PageCustomActionSchema = z.object({
  /**
   * Business identifier in camelCase. Drives the human-facing label key and
   * the React Query hook name (`use<PascalCase(code)>${Entity}`). MAY differ
   * from `endpoint` when the BA wants a clean UI label but the backend uses
   * a legacy route name (e.g. code='syncFromPce', endpoint='sync-from-proconcept').
   *
   * Accepted: camelCase or single-word lowercase. We allow letters & digits but
   * not dashes to keep TypeScript identifiers valid.
   */
  code: z
    .string()
    .min(1)
    .regex(/^[a-z][a-zA-Z0-9]*$/, 'code must be camelCase or a single lowercase word'),

  /**
   * Distinguishes API-bound actions from pure navigations.
   * - `api`      → emits backend endpoint + service method + React Query hook + button
   * - `navigate` → emits ONLY a button that calls `navigate(targetRoute)`
   *
   * Default `api` for legacy pagespecs that omit the field.
   */
  kind: z.enum(['api', 'navigate']).default('api'),

  /** Where the button renders. */
  scope: z.enum(['row', 'bulk', 'header']),

  /**
   * URL segment, verbatim. Used identically on both sides:
   *   - backend  →  [HttpVerb("<endpoint>")]
   *   - frontend →  apiClient.<verb>('/api/.../<endpoint>')
   *
   * Optional in the schema — when missing for `kind:api`, ba-create-prd
   * fills it with `defaultEndpoint(code)`. The audits enforce presence after
   * normalisation.
   */
  endpoint: z
    .string()
    .min(1)
    .regex(/^[a-z][a-z0-9-]*$/, 'endpoint must be kebab-case')
    .optional(),

  /** HTTP verb. Defaults POST (state mutations). GET for compute / read-only
   *  endpoints (analyze-impact). */
  httpMethod: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']).default('POST'),

  /**
   * C# DTO type names. null when there is no body / no response payload
   * (state-mutation endpoints like archive that return 204).
   */
  payloadDto: z.string().nullable().optional(),
  responseDto: z.string().nullable().default(null),

  /**
   * TypeScript counterparts. When omitted, scaffold-api-client falls back to
   * `payloadDto` / `responseDto` (they are usually identical PascalCase names
   * shared with the backend via codegen).
   */
  payloadType: z.string().nullable().optional(),
  responseType: z.string().nullable().default(null),

  /**
   * For `kind:navigate`: the screen the button leads to. ba-create-prd
   * resolves it into a concrete `targetRoute` string by looking it up in the
   * screen registry. One of `targetScreen` or `targetRoute` MUST be set.
   */
  targetScreen: z
    .string()
    .regex(/^SCR-[A-Z0-9_-]+$/, 'targetScreen must be SCR-...')
    .optional(),

  /**
   * For `kind:navigate`: the resolved route helper expression, e.g.
   * `routes.referentiels.typesAffaire.detail(item.id)`. scaffold-component
   * inlines this verbatim inside `onClick={() => navigate(<targetRoute>)}`.
   */
  targetRoute: z.string().min(1).optional(),

  /** i18n key for the button label. */
  labelKey: z.string().min(1),

  /**
   * Three-segment permission constant (`<module>.<section>.<action>`).
   * Drives the PermissionGuard wrapping in scaffold-component AND the
   * permissionAction segment passed to scaffold-controller / scaffold-business.
   */
  permission: z
    .string()
    .min(1)
    .regex(
      /^[a-z][a-z0-9-]*\.[a-z][a-z0-9-]*\.[a-z][a-z0-9-]*$/,
      'permission must be `<module>.<section>.<action>` kebab-case',
    ),

  /** Visual variant. Defaults `secondary` for custom actions, `primary` for the
   *  first header action (create), `danger` for destructive (delete). */
  variant: z.enum(['primary', 'secondary', 'danger']).optional(),

  /** UC-... back-reference (mandatory for kind:api so the stub carries a
   *  TODO[UC-…] anchor that audit-dev-api can verify). */
  ucReference: z
    .string()
    .regex(/^UC-[A-Z0-9_-]+$/, 'ucReference must be UC-...')
    .optional(),

  /** BR-... guards to enforce before performing the action. */
  guardRules: z.array(z.string().regex(/^BR-[A-Z0-9_-]+$/)).default([]),

  /** Optional state-machine transition. Populated by ba-create-prd from a
   *  linked workflow rule, consumed by scaffold-business. */
  workflowTransition: WorkflowTransitionSchema.optional(),

  /** Module names this action depends on (entities not yet in prd.entities.md).
   *  scaffold-business emits a `// BLOCKED[CROSS-MODULE: …]` TODO so audit-dev
   *  can flag the dependency. */
  crossModuleDeps: z.array(z.string().min(1)).default([]),
}).superRefine((action, ctx) => {
  if (action.kind === 'navigate') {
    if (!action.targetRoute && !action.targetScreen) {
      ctx.addIssue({
        code: 'custom',
        path: ['targetRoute'],
        message: 'navigate action requires targetRoute or targetScreen',
      })
    }
    if (action.endpoint) {
      ctx.addIssue({
        code: 'custom',
        path: ['endpoint'],
        message: 'navigate action must NOT carry endpoint — use targetRoute/targetScreen',
      })
    }
    if (action.payloadDto || action.payloadType) {
      ctx.addIssue({
        code: 'custom',
        path: ['payloadDto'],
        message: 'navigate action must NOT carry payloadDto/payloadType',
      })
    }
    if (action.workflowTransition) {
      ctx.addIssue({
        code: 'custom',
        path: ['workflowTransition'],
        message: 'navigate action must NOT carry workflowTransition',
      })
    }
  } else {
    // kind === 'api'
    if (action.targetScreen || action.targetRoute) {
      ctx.addIssue({
        code: 'custom',
        path: ['kind'],
        message: 'api action must NOT carry targetScreen/targetRoute — set kind:navigate instead',
      })
    }
  }
})

export type PageCustomAction = z.infer<typeof PageCustomActionSchema>

/**
 * Normalise a `PageCustomAction` after validation: fill missing defaults so
 * downstream consumers (ba-develop, audits) can rely on a stable shape.
 *
 * - `kind:api` without `endpoint` → `defaultEndpoint(code)`
 * - `kind:api` without `payloadType`/`responseType` → mirror the C# DTO names
 */
export function normalizePageCustomAction(action: PageCustomAction): PageCustomAction {
  if (action.kind === 'navigate') return action
  const endpoint = action.endpoint ?? defaultEndpoint(action.code)
  return {
    ...action,
    endpoint,
    payloadType: action.payloadType ?? action.payloadDto ?? null,
    responseType: action.responseType ?? action.responseDto ?? null,
  }
}

/**
 * Extract the permission's last segment, which maps to scaffold-controller's
 * `permissionAction` field (it joins it with the module/section to build
 * `<Mod>Permissions.<Section>.<PermissionAction>`).
 *
 *   lastPermissionSegment('referentiels.types-affaire.execute') === 'execute'
 */
export function lastPermissionSegment(permission: string): string {
  const parts = permission.split('.')
  return parts[parts.length - 1] ?? ''
}

/**
 * Build the expected URL path matched against axios calls in audit-dev-frontend
 * and against `[HttpVerb("…")]` attributes in audit-dev-api.
 *
 *   expectedUrlPath('header', 'sync-from-pce')      → '/sync-from-pce'
 *   expectedUrlPath('row', 'archive')               → '/{id}/archive'
 *   expectedUrlPath('bulk', 'export')               → '/bulk/export'
 *
 * The returned path is RELATIVE to the controller mount point
 * (`/api/<module>/<plural-kebab>`). Audits concatenate the prefix when
 * comparing against full URLs.
 */
export function expectedUrlPath(
  scope: PageCustomAction['scope'],
  endpoint: string,
): string {
  switch (scope) {
    case 'row':
      return `/{id}/${endpoint}`
    case 'bulk':
      return `/bulk/${endpoint}`
    case 'header':
      return `/${endpoint}`
  }
}

/**
 * Same as `expectedUrlPath` but uses the C# route-attribute syntax with the
 * `{id:guid}` constraint that scaffold-controller emits. Audit-dev-api uses
 * this form when checking `[HttpVerb("…")]` strings.
 */
export function expectedControllerRoute(
  scope: PageCustomAction['scope'],
  endpoint: string,
): string {
  switch (scope) {
    case 'row':
      return `{id:guid}/${endpoint}`
    case 'bulk':
      return `bulk/${endpoint}`
    case 'header':
      return endpoint
  }
}

/**
 * Filter a list of pagespec actions down to the custom (non-CRUD) ones —
 * used by every downstream consumer that derives a specific scaffolder input.
 */
export function customActionsOnly<T extends { code: string }>(actions: T[]): T[] {
  return actions.filter(a => !STANDARD_CRUD_CODES.has(a.code))
}
