/**
 * cli:scaffold-controller — generate.ts
 * Generates API controller + module-scoped permissions class.
 * Generated code consumes NuGet packages (SmartStack.Core, SmartStack.Application, SmartStack.Api).
 *
 * Per-page mode (post-Step 3 of the vertical-slice refactor):
 *  When `spec.customActions[]` is non-empty, each entry produces ONE extra
 *  controller method beyond the standard CRUD. Codes outside the canonical
 *  REST set (create/update/delete) are emitted as POST routes following the
 *  convention:
 *    - row scope    → POST /api/{plural}/{id:guid}/{code}
 *    - bulk scope   → POST /api/{plural}/bulk/{code}
 *    - header scope → POST /api/{plural}/{code}
 *  The bound service method is `_service.{Code}Async(...)` (canonical
 *  cross-layer naming — same as the React hook + frontend service stub).
 */
import { buildIntegrationRoute, pluralSegment } from '../../../../../lib/url-conventions.js'
import type {
  ScaffoldControllerInput,
  GeneratedFile,
  ControllerField,
  ControllerCustomAction,
} from './types.js'

export function generate(spec: ScaffoldControllerInput): GeneratedFile[] {
  const ns = spec.namespace ?? spec.appCode
  const mod = capitalize(spec.module)
  const e = spec.name
  const plural = spec.pluralName ?? e + 's'
  // Kebab-case plural — single source of truth shared with the frontend
  // api-client and with scaffold-screen-controller. Eliminates the historical
  // camelCase/kebab divergence on multi-word entities (OrderLines vs order-lines).
  const pluralKebab = pluralSegment(plural)
  // ASP.NET Core expects route attributes WITHOUT a leading slash.
  const integrationRouteAttr = buildIntegrationRoute(pluralKebab).replace(/^\//, '')
  const section = capitalize(spec.section)
  const permPrefix = spec.permissionPrefix ?? `${spec.module}.${spec.section}`
  const userFields = spec.fields.filter(f => !isSystemField(f.name))
  const createMapping = mapDtoToCommand(userFields, 'dto')
  const updateMapping = mapDtoToCommand(userFields, 'dto', 'id')

  const files: GeneratedFile[] = []

  const customActionMethods = spec.customActions
    .map(a => emitCustomActionMethod(a, mod, section, e))
    .join('\n')

  const controllerContent = `using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using SmartStack.Core.Navigation;
using SmartStack.Api.Authorization;
using ${ns}.Application.${mod}.DTOs;
using ${ns}.Application.${mod}.Interfaces;
using ${ns}.Application.${mod}.Commands;
using ${ns}.Application.${mod}.Queries;
using ${ns}.Api.Permissions.${mod};

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

/// <summary>
/// Integration controller — exposes generic CRUD over the entity for
/// machine-to-machine consumers (imports/exports, ETL, BI, third-party
/// integrations, dev tooling). The screen-driven API lives under
/// <c>/api/screens/...</c> and is generated by <c>scaffold-screen-controller</c>.
/// Both strata call the SAME business layer — business rules never duplicate.
/// </summary>
[ApiController]
[ApiExplorerSettings(GroupName = "integration")]
[Route("${integrationRouteAttr}")]
[NavRoute("${spec.navRoute}")]
[Authorize]
[Produces("application/json")]
public class ${plural}Controller : ControllerBase
{
    private readonly I${e}Service _service;

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

    [HttpGet]
    [RequirePermission(${mod}Permissions.${section}.Read)]
    public async Task<ActionResult<PaginatedResult<${e}ListDto>>> GetAll(
        [FromQuery] int page = 1,
        [FromQuery] int pageSize = 20,
        [FromQuery] string? search = null,
        CancellationToken ct = default)
    {
        var result = await _service.GetAllAsync(new Get${plural}Query(page, pageSize, search), ct);
        return Ok(result);
    }

    [HttpGet("{id:guid}")]
    [RequirePermission(${mod}Permissions.${section}.Read)]
    public async Task<ActionResult<${e}DetailDto>> GetById(Guid id, CancellationToken ct = default)
    {
        var result = await _service.GetByIdAsync(id, ct);
        return result is null ? NotFound() : Ok(result);
    }

    /// <summary>
    /// Lookup endpoint feeding the frontend EntityLookup combobox. Returns
    /// paginated {Id, DisplayName} pairs. Reuses the Read permission — anyone
    /// allowed to list the entity is allowed to search it.
    /// </summary>
    [HttpGet("lookup")]
    [RequirePermission(${mod}Permissions.${section}.Read)]
    public async Task<ActionResult<PaginatedResult<${e}RefDto>>> GetLookup(
        [FromQuery] string? search = null,
        [FromQuery] int page = 1,
        [FromQuery] int pageSize = 20,
        CancellationToken ct = default)
    {
        var result = await _service.GetLookupAsync(new Get${plural}LookupQuery(search, page, pageSize), ct);
        return Ok(result);
    }
${spec.actions.includes('create') ? `
    [HttpPost]
    [RequirePermission(${mod}Permissions.${section}.Create)]
    public async Task<ActionResult<Guid>> Create([FromBody] Create${e}Dto dto, CancellationToken ct = default)
    {
        var command = new Create${e}Command(${createMapping});
        var id = await _service.CreateAsync(command, ct);
        return CreatedAtAction(nameof(GetById), new { id }, id);
    }
` : ''}${spec.actions.includes('update') ? `
    [HttpPut("{id:guid}")]
    [RequirePermission(${mod}Permissions.${section}.Update)]
    public async Task<ActionResult> Update(Guid id, [FromBody] Update${e}Dto dto, CancellationToken ct = default)
    {
        var command = new Update${e}Command(${updateMapping});
        await _service.UpdateAsync(command, ct);
        return NoContent();
    }
` : ''}${spec.actions.includes('delete') ? `
    [HttpDelete("{id:guid}")]
    [RequirePermission(${mod}Permissions.${section}.Delete)]
    public async Task<ActionResult> Delete(Guid id, CancellationToken ct = default)
    {
        await _service.DeleteAsync(id, ct);
        return NoContent();
    }
` : ''}${customActionMethods}
}
`
  files.push({ path: `src/${ns}.Api/Controllers/${mod}/${plural}Controller.cs`, content: controllerContent })

  // Module-scoped namespace to avoid collisions across modules sharing section names.
  // Custom actions reuse the canonical permPrefix.{permissionAction} key — no
  // separate constant emitted because the action runs under an existing perm
  // (typically Update). This mirrors how DB seeds register them.
  const permContent = `namespace ${ns}.Api.Permissions.${mod};

public static class ${mod}Permissions
{
    public static class ${section}
    {
        public const string Access = "${permPrefix}.access";
${spec.actions.map(a => `        public const string ${capitalize(a)} = "${permPrefix}.${a}";`).join('\n')}
    }
}
`
  files.push({ path: `src/${ns}.Api/Permissions/${mod}/${mod}Permissions.${section}.cs`, content: permContent })

  return files
}

// ─── Helpers ───

function capitalize(s: string): string { return s.charAt(0).toUpperCase() + s.slice(1) }

/** Convert a kebab-case action code (`bulk-archive`) to PascalCase
 *  (`BulkArchive`) for C# method names. */
function pascalizeCode(code: string): string {
  return code
    .split('-')
    .map(part => capitalize(part))
    .join('')
}

function isSystemField(name: string): boolean {
  const systemFields = ['id', 'createdat', 'updatedat', 'deletedat', 'createdby', 'updatedby', 'tenantid']
  return systemFields.includes(name.toLowerCase())
}

function mapDtoToCommand(fields: ControllerField[], dtoVar: string, prependId?: string): string {
  const fieldArgs = fields.map(f => `${dtoVar}.${capitalize(f.name)}`)
  if (prependId) {
    return [prependId, ...fieldArgs].join(', ')
  }
  return fieldArgs.join(', ')
}

/**
 * Map an HTTP verb to its `[HttpXxx]` route attribute.
 *   POST   → [HttpPost(...)]
 *   GET    → [HttpGet(...)]
 *   PUT    → [HttpPut(...)]
 *   PATCH  → [HttpPatch(...)]
 *   DELETE → [HttpDelete(...)]
 */
function httpAttr(verb: string, route: string): string {
  const pascal = verb.charAt(0).toUpperCase() + verb.slice(1).toLowerCase()
  return `[Http${pascal}("${route}")]`
}

/**
 * Emits a single custom-action controller method. Maps:
 *   - row scope    → {VERB} {id:guid}/{code} with `Guid id` parameter
 *   - bulk scope   → {VERB} bulk/{code}
 *   - header scope → {VERB} {code} with no id
 *
 * The VERB defaults to POST but may be GET (compute / read-only endpoints
 * like `analyze-impact`), PUT/PATCH (rare — partial updates) or DELETE.
 * `[FromBody]` is OMITTED when the verb is GET, even if `payloadDto` is set
 * (ASP.NET Core does not allow bodies on GET methods).
 *
 * The service call uses the canonical `_service.{Pascal}Async(...)` shape so
 * scaffold-business (Step 4) can emit a matching method and the two layers
 * line up by construction.
 *
 * Permission constant: `{Mod}Permissions.{Section}.{PermissionAction}` —
 * defaults to the existing Update / Create / Delete perm for state mutations.
 */
function emitCustomActionMethod(
  action: ControllerCustomAction,
  mod: string,
  section: string,
  entity: string,
): string {
  const methodName = pascalizeCode(action.code)
  const permConst = `${mod}Permissions.${section}.${capitalize(action.permissionAction)}`
  const responseType = action.responseDto === 'NoContent'
    ? 'ActionResult'
    : `ActionResult<${action.responseDto}>`

  // Defensive fallback — Zod's `.default('POST')` only fires on `.parse()`.
  // Tests and legacy callers may instantiate the object literally; we keep
  // POST as the implicit default to preserve the pre-httpMethod behaviour.
  const verb = action.httpMethod ?? 'POST'
  const allowBody = verb !== 'GET' && Boolean(action.payloadDto)

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

  if (action.scope === 'row') {
    routeAttr = httpAttr(verb, `{id:guid}/${action.code}`)
    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(verb, `bulk/${action.code}`)
    if (allowBody) params.push(`[FromBody] ${action.payloadDto} dto`)
    serviceArgs = allowBody ? 'dto, ct' : 'ct'
  } else {
    // header scope
    routeAttr = httpAttr(verb, action.code)
    if (allowBody) params.push(`[FromBody] ${action.payloadDto} dto`)
    serviceArgs = allowBody ? 'dto, ct' : 'ct'
  }
  params.push('CancellationToken ct = default')

  // Body — call the service then return Ok(result) for typed responses,
  // NoContent() for void endpoints.
  const body = action.responseDto === 'NoContent'
    ? `        await _service.${methodName}Async(${serviceArgs});
        return NoContent();`
    : `        var result = await _service.${methodName}Async(${serviceArgs});
        return Ok(result);`

  // Note: comment hints scaffold-business will produce the matching service
  // method. The two layers stay in lockstep via the action.code single-source-of-truth.
  return `
    ${routeAttr}
    [RequirePermission(${permConst})]
    public async Task<${responseType}> ${methodName}(${params.join(', ')})
    {
${body}
    }
`
}
