/**
 * scaffold-screen-controller — generate.ts
 *
 * Emits the screen-driven controller stratum for one (section, entity) pair.
 *
 *   src/{Ns}.Api/Controllers/{Module}/Screens/{EntityPlural}ScreenController.cs
 *   src/{Ns}.Application/{Module}/DTOs/Screens/{Entity}{View}ScreenDto.cs  × N
 *
 * One endpoint per pagespec. Each endpoint:
 *   - lives under   /api/screens/{entityPluralLower}/...
 *   - carries       [ApiExplorerSettings(GroupName = "screens")]   (class-level)
 *                   [RequirePermission(...)]                       (method-level)
 *   - calls         _service.{Method}Async(...)
 *
 * The service interface (I{Entity}Service) is assumed to already expose:
 *   - GetForListScreenAsync / GetForDetailScreenAsync       (read screens)
 *   - CreateAsync / UpdateAsync                              (form screens, REUSED from integration)
 *   - {Pascal(action.code)}Async                             (custom actions, REUSED)
 *
 * When the scaffolder cannot prove the method exists (cheap heuristic: it is
 * never seen in any pagespec.action), it appends a `// TODO[SCREEN-{code}]:`
 * marker on the controller's call site so audit-dev-api can block the merge.
 *
 * Pure function — no I/O, no filesystem. Returns an array of GeneratedFile.
 */

import { pluralize, toPascalCase } from '../../../../../lib/string-utils.js'
import { buildScreenRoute, pluralSegment } from '../../../../../lib/url-conventions.js'
import type { GeneratedFile, PageSpec, PageSpecAction, PageSpecColumn, ScaffoldScreenControllerSpec } from './types.js'

export interface GenerateOutput {
  files: GeneratedFile[]
  todos: string[]
}

export function generate(spec: ScaffoldScreenControllerSpec, pagespecs: PageSpec[]): GenerateOutput {
  const e = spec.entity                                   // PascalCase singular
  const ePlural = pluralize(e)                            // PascalCase plural
  const eLower = lowercaseFirst(e)
  // Kebab-case plural — single source of truth shared with scaffold-controller
  // and scaffold-api-client. Replaces the prior `lowercaseFirst(pluralize)` which
  // gave camelCase URLs (`orderLines`) — diverged from the frontend's kebab
  // (`order-lines`) on every multi-word entity.
  const ePluralKebab = pluralSegment(ePlural)             // URL segment
  // ASP.NET Core wants the route attribute without a leading slash.
  const screenRouteAttr = buildScreenRoute(ePluralKebab).replace(/^\//, '')
  const mod = toPascalCase(spec.module)
  const ns = spec.namespace
  const controllerClass = `${ePlural}ScreenController`
  const controllerPath = `src/${ns}.Api/Controllers/${mod}/Screens/${controllerClass}.cs`

  const files: GeneratedFile[] = []
  const todos: string[] = []

  // ─── Emit per-pagespec endpoint blocks + DTOs ───
  // Each pagespec contributes (a) its view's main method (list/detail/form),
  // PLUS (b) any custom actions (kind="api") attached to it. Custom actions
  // are emitted regardless of view — they live alongside the read/write
  // endpoint(s) of that screen.
  const methods: string[] = []
  for (const ps of pagespecs) {
    const emitted = emitMethodAndDto({ pagespec: ps, entity: e, entityLower: eLower, mod, ns, todos })
    if (emitted.method) methods.push(emitted.method)
    if (emitted.dto) files.push(emitted.dto)
    const defaultPerm = permissionConstant(ps.permission, mod)
    const customMethods = (ps.actions ?? [])
      .filter(a => a.kind === 'api')
      .map(a => emitCustomActionMethod({ entity: e, screenCode: ps.screenCode, action: a, mod, defaultPerm }))
    methods.push(...customMethods)
  }

  // ─── Assemble the controller ───
  const usings = [
    'using Microsoft.AspNetCore.Authorization;',
    'using Microsoft.AspNetCore.Mvc;',
    'using SmartStack.Api.Authorization;',
    `using ${ns}.Application.${mod}.DTOs;`,
    `using ${ns}.Application.${mod}.DTOs.Screens;`,
    `using ${ns}.Application.${mod}.Interfaces;`,
    `using ${ns}.Application.${mod}.Commands;`,
    `using ${ns}.Application.${mod}.Queries;`,
    `using ${ns}.Api.Permissions.${mod};`,
  ].join('\n')

  const controllerContent = `${usings}

namespace ${ns}.Api.Controllers.${mod}.Screens;

/// <summary>
/// Screen-driven controller — exposes one endpoint per screen of section
/// "${spec.section}" (entity ${e}). Companion of the integration controller
/// (${ePlural}Controller, /api/v1/integration/...). Both call the same
/// I${e}Service — business rules NEVER duplicate between strata.
/// </summary>
[ApiController]
[ApiExplorerSettings(GroupName = "screens")]
[Route("${screenRouteAttr}")]
[Authorize]
[Produces("application/json")]
public class ${controllerClass} : ControllerBase
{
    private readonly I${e}Service _service;

    public ${controllerClass}(I${e}Service service) => _service = service;

${methods.join('\n\n')}
}
`
  files.push({ path: controllerPath, content: controllerContent })

  return { files, todos }
}

// ─── Per-pagespec emission ───────────────────────────────────────────────

interface EmitArgs {
  pagespec: PageSpec
  entity: string         // PascalCase singular
  entityLower: string
  mod: string            // PascalCase module
  ns: string             // PascalCase namespace
  todos: string[]
}

interface EmittedPart {
  method: string | null
  dto: GeneratedFile | null
}

function emitMethodAndDto(args: EmitArgs): EmittedPart {
  const { pagespec: ps, entity: e, mod, ns } = args
  const view = ps.view
  const permConst = permissionConstant(ps.permission, mod)
  const screenCode = ps.screenCode

  switch (view) {
    case 'list':
      return {
        method: emitListMethod({ entity: e, permConst, screenCode }),
        dto: emitListDto({ entity: e, mod, ns, columns: ps.columns ?? [] }),
      }
    case 'detail':
      return {
        method: emitDetailMethod({ entity: e, permConst, screenCode }),
        dto: emitDetailDto({ entity: e, mod, ns, columns: ps.columns ?? [] }),
      }
    case 'form':
      return {
        method: emitFormMethods({ entity: e, permConst, screenCode }),
        dto: null, // Form consumes Create/UpdateDto from integration — never re-emit
      }
    case 'dashboard':
      // Wave F3 (2026-05-27) — dashboard view emits TWO endpoints under
      // /api/screens/{plural}/dashboard/* that call the same service methods
      // already scaffolded by scaffold-business when `hasDashboard: true`.
      // Both DTOs (`${E}DashboardConsolidatedDto`, `${E}DashboardAlertDto`)
      // live in the integration stratum — never re-emitted here.
      return {
        method: emitDashboardMethods({ entity: e, permConst, screenCode }),
        dto: null,
      }
    default:
      // Hub views (app-home / module-home / section-home) are pure Slot
      // containers on the frontend — `scaffold-component` emits NO useService
      // hook and NO api.get/post call for them. Emitting no backend endpoint
      // here is therefore correct AND intentional. Locked by hub-views.test.ts.
      // No TODO is pushed — there is no debt; the absence is by design.
      if (view === 'app-home' || view === 'module-home' || view === 'section-home') {
        return { method: null, dto: null }
      }
      // kanban / card — MVP scope: same data set as list, different
      // visualisation. Fall back to a `[HttpGet("list")]` endpoint so the
      // frontend's getAll() call always lands on a real route. The TODO
      // surfaces to audit-dev-api so a dedicated emitter can be built later
      // (e.g. swim-lane group-by for kanban).
      args.todos.push(
        `[SCREEN-${screenCode}] view "${view}" reuses GET /list as MVP fallback. Dedicated emitter pending.`,
      )
      return {
        method: emitListMethod({ entity: e, permConst, screenCode }),
        dto: emitListDto({ entity: e, mod, ns, columns: ps.columns ?? [] }),
      }
  }
}

// ─── Method emitters per view ────────────────────────────────────────────

function emitListMethod(args: { entity: string; permConst: string; screenCode: string }): string {
  const { entity, permConst, screenCode } = args
  const dtoName = `${entity}ListScreenDto`
  const queryName = `Get${entity}ListScreenQuery`
  return `    /// <summary>List screen — ${screenCode}.</summary>
    [HttpGet("list")]
    [RequirePermission(${permConst})]
    public async Task<ActionResult<PaginatedResult<${dtoName}>>> GetList(
        [FromQuery] int page = 1,
        [FromQuery] int pageSize = 20,
        [FromQuery] string? search = null,
        CancellationToken ct = default)
    {
        var result = await _service.GetForListScreenAsync(new ${queryName}(page, pageSize, search), ct);
        return Ok(result);
    }`
}

function emitDetailMethod(args: { entity: string; permConst: string; screenCode: string }): string {
  const { entity, permConst, screenCode } = args
  const dtoName = `${entity}DetailScreenDto`
  return `    /// <summary>Detail screen — ${screenCode}.</summary>
    [HttpGet("detail/{id:guid}")]
    [RequirePermission(${permConst})]
    public async Task<ActionResult<${dtoName}>> GetDetail(Guid id, CancellationToken ct = default)
    {
        var result = await _service.GetForDetailScreenAsync(id, ct);
        return result is null ? NotFound() : Ok(result);
    }`
}

function emitFormMethods(args: { entity: string; permConst: string; screenCode: string }): string {
  const { entity, permConst, screenCode } = args
  // Create and Update permissions are derived from the base permission ("read") by swapping
  // the last segment. Until we read RBAC here, we reuse permConst and add a doc note.
  const createPerm = permConst.replace(/\.Read$/, '.Create').replace(/\.Update$/, '.Create')
  const updatePerm = permConst.replace(/\.Read$/, '.Update').replace(/\.Create$/, '.Update')
  return `    /// <summary>Form screen (create) — ${screenCode}. Calls existing CreateAsync on the service.</summary>
    [HttpPost("form")]
    [RequirePermission(${createPerm})]
    public async Task<ActionResult<Guid>> Create([FromBody] Create${entity}Dto dto, CancellationToken ct = default)
    {
        var id = await _service.CreateAsync(new Create${entity}Command(/* TODO[SCREEN-${screenCode}]: map dto → command */), ct);
        return CreatedAtAction(nameof(GetDetailRoute), new { id }, id);
    }

    /// <summary>Form screen (update) — ${screenCode}. Calls existing UpdateAsync on the service.</summary>
    [HttpPut("form/{id:guid}")]
    [RequirePermission(${updatePerm})]
    public async Task<ActionResult> Update(Guid id, [FromBody] Update${entity}Dto dto, CancellationToken ct = default)
    {
        await _service.UpdateAsync(new Update${entity}Command(id /* TODO[SCREEN-${screenCode}]: map dto → command */), ct);
        return NoContent();
    }

    /// <summary>Internal route token used by CreatedAtAction (form Create above).
    /// The actual GET detail endpoint is on this controller too (see GetDetail).</summary>
    private static string GetDetailRoute => nameof(GetDetail);`
}

function emitDashboardMethods(args: { entity: string; permConst: string; screenCode: string }): string {
  const { entity, permConst, screenCode } = args
  const consolidatedDto = `${entity}DashboardConsolidatedDto`
  const alertDto = `${entity}DashboardAlertDto`
  return `    /// <summary>Dashboard — consolidated KPIs for ${screenCode}.</summary>
    [HttpGet("dashboard/consolidated")]
    [RequirePermission(${permConst})]
    public async Task<ActionResult<${consolidatedDto}>> GetDashboardConsolidated(
        [FromQuery] DateTime? startDate = null,
        [FromQuery] DateTime? endDate = null,
        CancellationToken ct = default)
    {
        var result = await _service.GetDashboardConsolidatedAsync(startDate, endDate, ct);
        return Ok(result);
    }

    /// <summary>Dashboard — active alerts for ${screenCode}.</summary>
    [HttpGet("dashboard/alerts")]
    [RequirePermission(${permConst})]
    public async Task<ActionResult<IReadOnlyList<${alertDto}>>> GetDashboardAlerts(CancellationToken ct = default)
    {
        var result = await _service.GetDashboardAlertsAsync(ct);
        return Ok(result);
    }`
}

function emitCustomActionMethod(args: { entity: string; screenCode: string; action: PageSpecAction; mod: string; defaultPerm: string }): string {
  const { action, defaultPerm, screenCode } = args
  const endpoint = action.endpoint ?? toKebab(action.code)
  // toPascalCase handles both kebab ("submit-for-review" → "SubmitForReview")
  // and camelCase ("syncFromPce" → "SyncFromPce"). The action.code remains the
  // single source of truth — endpoint segment may differ (e.g. legacy URLs).
  const methodName = toPascalCase(action.code)
  const verb = action.httpMethod ?? 'POST'
  const httpAttr = `[Http${verb.charAt(0) + verb.slice(1).toLowerCase()}`
  const permConst = action.permission ? permissionConstant(action.permission, args.mod) : defaultPerm
  const responseType = action.responseDto && action.responseDto !== 'NoContent'
    ? `ActionResult<${action.responseDto}>`
    : 'ActionResult'
  const allowBody = verb !== 'GET' && action.payloadDto != null && action.payloadDto !== ''

  let routeAttr: string
  const params: string[] = []
  let serviceArgs: string

  if (action.scope === 'row') {
    routeAttr = `${httpAttr}("detail/{id:guid}/${endpoint}")]`
    params.push('Guid id')
    if (allowBody) params.push(`[FromBody] ${action.payloadDto} dto`)
    serviceArgs = allowBody ? 'id, dto, ct' : 'id, ct'
  } else if (action.scope === 'bulk') {
    routeAttr = `${httpAttr}("bulk/${endpoint}")]`
    if (allowBody) params.push(`[FromBody] ${action.payloadDto} dto`)
    serviceArgs = allowBody ? 'dto, ct' : 'ct'
  } else {
    // header scope (default)
    routeAttr = `${httpAttr}("${endpoint}")]`
    if (allowBody) params.push(`[FromBody] ${action.payloadDto} dto`)
    serviceArgs = allowBody ? 'dto, ct' : 'ct'
  }
  params.push('CancellationToken ct = default')

  const body = responseType === 'ActionResult'
    ? `        await _service.${methodName}Async(${serviceArgs});
        return NoContent();`
    : `        var result = await _service.${methodName}Async(${serviceArgs});
        return Ok(result);`

  return `    /// <summary>Custom action ${action.code} (${action.scope}) — ${screenCode}.</summary>
    ${routeAttr}
    [RequirePermission(${permConst})]
    public async Task<${responseType}> ${methodName}(${params.join(', ')})
    {
${body}
    }`
}

// ─── DTO emitters ────────────────────────────────────────────────────────

function emitListDto(args: { entity: string; mod: string; ns: string; columns: PageSpecColumn[] }): GeneratedFile {
  const { entity, mod, ns, columns } = args
  const className = `${entity}ListScreenDto`
  const path = `src/${ns}.Application/${mod}/DTOs/Screens/${className}.cs`
  const props = columns.length > 0
    ? columns.map(c => `    public ${dotnetTypeFor(c)} ${toPascalCase(c.key)} { get; init; } = ${dotnetDefaultFor(c)};`).join('\n')
    : '    // No columns in pagespec — backend exposes only Id + CreatedAt as a safe minimum.'

  return {
    path,
    content: `namespace ${ns}.Application.${mod}.DTOs.Screens;

/// <summary>
/// Shape of one row in the ${entity} list screen — fields derived from
/// the pagespec columns[], not the full entity. Keep stable: any change
/// here is a contract change with the frontend.
/// </summary>
public record ${className}
{
    public Guid Id { get; init; }
${props}
    public DateTime CreatedAt { get; init; }
}
`,
  }
}

function emitDetailDto(args: { entity: string; mod: string; ns: string; columns: PageSpecColumn[] }): GeneratedFile {
  const { entity, mod, ns, columns } = args
  const className = `${entity}DetailScreenDto`
  const path = `src/${ns}.Application/${mod}/DTOs/Screens/${className}.cs`
  // For detail, we still derive from columns[] in the MVP — the BA author can
  // enrich the detail pagespec with extra columns specific to the detail view.
  // Future refinement: a dedicated `fields[]` block on the detail pagespec.
  const props = columns.length > 0
    ? columns.map(c => `    public ${dotnetTypeFor(c)} ${toPascalCase(c.key)} { get; init; } = ${dotnetDefaultFor(c)};`).join('\n')
    : '    // No columns in pagespec — backend exposes only Id + timestamps.'

  return {
    path,
    content: `namespace ${ns}.Application.${mod}.DTOs.Screens;

/// <summary>
/// Shape of the ${entity} detail screen — fields derived from the pagespec.
/// </summary>
public record ${className}
{
    public Guid Id { get; init; }
${props}
    public DateTime CreatedAt { get; init; }
    public DateTime UpdatedAt { get; init; }
}
`,
  }
}

// ─── Helpers ─────────────────────────────────────────────────────────────

function lowercaseFirst(s: string): string {
  return s.length > 0 ? s.charAt(0).toLowerCase() + s.slice(1) : s
}

function toKebab(s: string): string {
  return s
    .replace(/([a-z])([A-Z])/g, '$1-$2')
    .replace(/_/g, '-')
    .toLowerCase()
}

/** Convert `module.section.action` → `{Mod}Permissions.{Section}.{Action}`.
 *  The Mod argument is the PascalCase module the controller belongs to (not
 *  necessarily the same as the permission's module segment — sections share
 *  permission scopes across modules sometimes). */
function permissionConstant(permission: string, mod: string): string {
  const parts = permission.split('.')
  if (parts.length !== 3) {
    // Defensive: malformed permission strings are passed through verbatim
    // wrapped in a comment so the C# compiler surfaces the error early.
    return `/* malformed permission: ${permission} */ ${mod}Permissions.Unknown.Unknown`
  }
  const [, section, action] = parts
  return `${mod}Permissions.${toPascalCase(section)}.${toPascalCase(action)}`
}

/** Map a pagespec column to a C# type — best-effort heuristic on formatHint. */
function dotnetTypeFor(c: PageSpecColumn): string {
  const hint = (c.formatHint ?? '').toLowerCase()
  if (hint === 'currency' || hint === 'number' || hint === 'decimal') return 'decimal'
  if (hint === 'integer' || hint === 'count') return 'int'
  if (hint === 'date' || hint === 'datetime') return 'DateTime'
  if (hint === 'bool' || hint === 'boolean') return 'bool'
  if (hint === 'guid' || hint === 'uuid') return 'Guid'
  return 'string'
}

function dotnetDefaultFor(c: PageSpecColumn): string {
  const t = dotnetTypeFor(c)
  switch (t) {
    case 'decimal':
    case 'int':
      return '0'
    case 'DateTime':
      return 'default'
    case 'bool':
      return 'false'
    case 'Guid':
      return 'Guid.Empty'
    default:
      return '""'
  }
}
