/**
 * uat-plan/discover-endpoints.ts — Parse generated controllers into the API axis.
 *
 * PURE parse (parseController) + a thin I/O wrapper (discoverEndpoints) that reads
 * every `*Controller.cs` under the API project. The structural contract (verified
 * against SmartStack.app + the scaffold-controller/screen-controller templates):
 *   - class-level   [Route("api/...")]  (literal) and/or [NavRoute("a.b")]
 *   - per action    [HttpGet] / [HttpPost("{id:guid}")] / [HttpDelete("{id:guid}")]
 *   - per action    [RequirePermission(<C# const expr>)]  (on the line(s) BELOW the verb)
 *
 * The permission is a C# CONSTANT reference (e.g. `Permissions.Admin.Users.View`),
 * not a dot-path string — we capture it verbatim as `permissionExpr` and let
 * discover.ts resolve it to a permission path against the live permission set.
 */
import { readText } from '../../../lib/fs.js';
import { findControllerFiles } from '../../../lib/detector.js';
import { extractNavRoutes } from '../../../lib/navroute-parser.js';

export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';

export interface ParsedEndpoint {
  controller: string;
  method: HttpMethod;
  /** Full API route, e.g. "/api/administration/users" or ".../users/{id}". */
  route: string;
  /** Controller [NavRoute("a.b")] (or inferred from [Route]) — ties to a nav path. */
  navRoute?: string;
  /** Raw [RequirePermission(<expr>)] argument, verbatim. */
  permissionExpr?: string;
  /** Success status from [ProducesResponseType(..., StatusCodes.Status2xx)] (default 200). */
  okStatus?: number;
  /** Action carries [AllowAnonymous]. */
  allowAnonymous?: boolean;
}

const VERB_RE = /\[Http(Get|Post|Put|Patch|Delete)(?:\(\s*"([^"]*)"\s*\))?\]/;
const REQUIRE_PERM_RE = /\[RequirePermission\(\s*([^)]+?)\s*\)\]/;
/** A C# method signature line: `public <returntype> Name(` (not a property/ctor body). */
const METHOD_SIG_RE = /\bpublic\b[^;{]*\b\w+\s*\(/;
/** The success status declared by a `[ProducesResponseType(..., StatusCodes.Status2xx)]`. */
const PRODUCES_2XX_RE = /StatusCodes\.Status(2\d\d)/;

/** Drop EF/MVC route constraints: `{id:guid}` → `{id}`. */
function stripRouteConstraints(t: string): string {
  return t.replace(/\{(\w+):[^}]+\}/g, '{$1}');
}

/** Resolve the controller's base API path: literal [Route] wins, else `api/<navRoute>`. */
function resolveBase(classRoute: string | undefined, navRoute: string | undefined): string {
  if (classRoute && !classRoute.includes('[')) {
    return `/${classRoute.replace(/^\/+/, '').replace(/\/+$/, '')}`;
  }
  if (navRoute) return `/api/${navRoute.replace(/\./g, '/')}`;
  return '/api';
}

/** Append a method template to the base route. */
function joinRoute(base: string, template: string | undefined): string {
  if (!template) return base;
  return `${base}/${stripRouteConstraints(template).replace(/^\/+/, '')}`;
}

/**
 * Parse one controller file's content into its endpoints. PURE.
 *
 * For each method signature we gather the CONTIGUOUS attribute block immediately
 * above it (skipping blank/comment lines) and read verb/route, permission, success
 * status and the anonymous flag from that block — order-independent, so it is
 * robust to whether [RequirePermission]/[ProducesResponseType]/[AllowAnonymous]
 * sit above or below the [Http*] verb.
 */
export function parseController(content: string): ParsedEndpoint[] {
  const controller = content.match(/class\s+(\w+Controller)\b/)?.[1] ?? 'UnknownController';
  const navRoute = extractNavRoutes(content)[0]?.navRoute;
  const classRoute = content.match(/\[Route\(\s*"([^"]+)"\s*\)\]/)?.[1];
  const base = resolveBase(classRoute, navRoute);

  const lines = content.split(/\r?\n/);
  const out: ParsedEndpoint[] = [];

  for (let i = 0; i < lines.length; i++) {
    if (!METHOD_SIG_RE.test(lines[i])) continue;

    // Walk up to gather the action's attribute block; stop at the first line that
    // is neither an attribute nor a blank/comment (a brace, the class decl, or the
    // previous method's body).
    const lines_above: string[] = [];
    for (let k = i - 1; k >= 0; k--) {
      const t = lines[k].trim();
      if (t.startsWith('[')) lines_above.push(t);
      else if (t === '' || t.startsWith('//') || t.startsWith('*') || t.startsWith('/*')) continue;
      else break;
    }
    const attrs = lines_above.join('\n');

    const verb = attrs.match(VERB_RE);
    if (!verb) continue; // not an HTTP action (ctor, helper, …)

    const endpoint: ParsedEndpoint = {
      controller,
      method: verb[1].toUpperCase() as HttpMethod,
      route: joinRoute(base, verb[2]),
    };
    if (navRoute) endpoint.navRoute = navRoute;
    const perm = attrs.match(REQUIRE_PERM_RE);
    if (perm) endpoint.permissionExpr = perm[1].trim();
    const ok = attrs.match(PRODUCES_2XX_RE);
    if (ok) endpoint.okStatus = parseInt(ok[1], 10);
    if (/\[AllowAnonymous\]/.test(attrs)) endpoint.allowAnonymous = true;
    out.push(endpoint);
  }
  return out;
}

/** Read + parse every controller under the API project. */
export async function discoverEndpoints(apiDir: string): Promise<ParsedEndpoint[]> {
  const files = await findControllerFiles(apiDir);
  const out: ParsedEndpoint[] = [];
  for (const file of files) {
    out.push(...parseController(await readText(file)));
  }
  return out;
}
