/**
 * uat-plan/project-roles.ts — PURE projection of discovery data onto roles.
 *
 * Turns the plain DiscoveryResult into the plan's `routes[]` and `endpoints[]`:
 *   - each nav node expands into one Route per registered view (list/detail/create/…),
 *   - every Route/Endpoint carries the ground-truth `access` / `expected_by_role`
 *     map computed by rbac-matrix (wildcard-aware).
 *
 * No I/O, no Date, no randomness — same input, same output (byte-stable plans).
 * Route/URL/strategy rules mirror the frontend contract in scaffold-routes:
 *   list/section-home → base URL (menu_click); detail → /:id, edit → /:id/edit
 *   (click_row_in_parent, never goto+seedId — F7); create → /create (goto).
 * In M1 `subsets[]` is empty: implicit suffixes are ROUTES, not in-page elements;
 * the dynamic in-page subsets (tabs/buttons) are added by M6.
 */
import { buildAccessMap, ANONYMOUS_ROLE } from '../lib/rbac-matrix.js';
import type { Route, Endpoint } from '../lib/plantest-schema.js';
import type { DiscoveredNavRoute, DiscoveredEndpoint, ImplicitView } from './types.js';

/** Privilege-descending order for the well-known SmartStack roles. */
const ROLE_PRIORITY = ['super-admin', 'admin', 'manager', 'contributor', 'viewer'];

/**
 * Canonical, deterministic role ordering: known roles by descending privilege,
 * unknown roles ASCII-sorted after them, `anonymous` always last. Dedupes.
 */
export function canonicalRoleOrder(roles: readonly string[]): string[] {
  const uniq = [...new Set(roles)];
  const rest = uniq.filter((r) => r !== ANONYMOUS_ROLE);
  const anon = uniq.filter((r) => r === ANONYMOUS_ROLE);
  rest.sort((a, b) => {
    const ia = ROLE_PRIORITY.indexOf(a);
    const ib = ROLE_PRIORITY.indexOf(b);
    if (ia !== -1 && ib !== -1) return ia - ib;
    if (ia !== -1) return -1;
    if (ib !== -1) return 1;
    return a < b ? -1 : a > b ? 1 : 0;
  });
  return [...rest, ...anon];
}

/** Views that render the node's BASE route (no implicit suffix). */
const BASE_VIEWS = new Set<ImplicitView>(['list', 'section-home', 'module-home']);

/** UPPER_SNAKE route id from a component key + view (unique by construction; invariant 1). */
export function routeIdFor(componentKey: string, view: ImplicitView): string {
  const base = componentKey.replace(/[^A-Za-z0-9]+/g, '_').toUpperCase();
  if (BASE_VIEWS.has(view)) return base;
  return `${base}_${String(view).replace(/[^A-Za-z0-9]+/g, '_').toUpperCase()}`;
}

/** Component key for a view (base key for base views, `${key}.${view}` for suffix pages). */
export function componentKeyFor(componentKey: string, view: ImplicitView): string {
  return BASE_VIEWS.has(view) ? componentKey : `${componentKey}.${view}`;
}

/**
 * Implicit-suffix → URL-pattern map, MIRRORING @atlashub/smartstack DynamicRouter's
 * IMPLICIT_SUFFIXES. Most suffixes are id-bearing (`/:id/...`); create/new/import are
 * collection-level. Base views (list/section-home/module-home) render the bare route.
 */
const SUFFIX_URL: Record<string, string> = {
  detail: '/:id',
  edit: '/:id/edit',
  create: '/create',
  new: '/new',
  duplicate: '/:id/duplicate',
  settings: '/:id/settings',
  configure: '/:id/configure',
  permissions: '/:id/permissions',
  members: '/:id/members',
  history: '/:id/history',
  logs: '/:id/logs',
  analytics: '/:id/analytics',
  preview: '/:id/preview',
  versions: '/:id/versions',
  comments: '/:id/comments',
  attachments: '/:id/attachments',
  audit: '/:id/audit',
  export: '/:id/export',
  notifications: '/:id/notifications',
  schedule: '/:id/schedule',
  workflow: '/:id/workflow',
  summary: '/:id/summary',
  test: '/:id/test',
  runs: '/:id/runs',
  import: '/import',
};

/** The implicit view suffixes — registry keys `${node}.${suffix}` are checked against these. */
export const IMPLICIT_VIEW_SUFFIXES: readonly string[] = Object.keys(SUFFIX_URL);

/** Final URL for a view, mirroring the DynamicRouter IMPLICIT_SUFFIXES contract. */
export function urlForView(baseRoute: string, view: ImplicitView): string {
  if (BASE_VIEWS.has(view)) return baseRoute;
  const suffix = SUFFIX_URL[view as string];
  return suffix ? `${baseRoute}${suffix}` : `${baseRoute}/${view}`;
}

/** Navigation strategy: id-bearing (`/:id`) pages click a row in the parent list (F7). */
export function navStrategyForView(view: ImplicitView): Route['navigation_strategy'] {
  if (BASE_VIEWS.has(view)) return 'menu_click';
  const suffix = SUFFIX_URL[view as string];
  return suffix && suffix.includes('/:id') ? 'click_row_in_parent' : 'goto';
}

/** Expand one nav node into its plan Routes — one per registered view. */
export function expandNavRoute(
  node: DiscoveredNavRoute,
  roles: readonly string[],
  grantsByRole: Readonly<Record<string, readonly { path: string; isWildcard: boolean }[]>>,
): Route[] {
  const access = buildAccessMap(roles, node.permission, grantsByRole);
  const seen = new Set<ImplicitView>();
  const out: Route[] = [];

  for (const view of node.views) {
    if (seen.has(view)) continue;
    seen.add(view);

    const strategy = navStrategyForView(view);
    const route: Route = {
      id: routeIdFor(node.componentKey, view),
      component_key: componentKeyFor(node.componentKey, view),
      route: urlForView(node.baseRoute, view),
      navigation_strategy: strategy,
      expect: {
        access_by_role: true,
        no_console_error: true,
        ...(node.label ? { title: node.label } : {}),
      },
      access: { ...access },
      subsets: [],
    };
    if (node.permission) route.permission = node.permission;
    if (strategy === 'click_row_in_parent') {
      route.parent_route = node.baseRoute;
      route.on_empty_list = 'INDETERMINATE';
    }
    out.push(route);
  }
  return out;
}

/** Project every nav node onto roles, preserving discovery order. */
export function projectRoutes(
  routes: readonly DiscoveredNavRoute[],
  roles: readonly string[],
  grantsByRole: Readonly<Record<string, readonly { path: string; isWildcard: boolean }[]>>,
): Route[] {
  return routes.flatMap((node) => expandNavRoute(node, roles, grantsByRole));
}

/** Fallback success status when a controller declares no ProducesResponseType (creates→201 come from the parse). */
export function okStatusFor(_method: DiscoveredEndpoint['method']): number {
  return 200;
}

/** Strip the `/api/` (or leading `/`) prefix to a stable endpoint id path. */
function endpointIdPath(route: string): string {
  return route.replace(/^\/?api\//i, '').replace(/^\//, '');
}

/** Expected status per role: anonymous→401, granted (or ungated)→ok, otherwise→403. */
export function expectedStatusByRole(
  roles: readonly string[],
  permission: string | undefined,
  grantsByRole: Readonly<Record<string, readonly { path: string; isWildcard: boolean }[]>>,
  okStatus: number,
): Record<string, number> {
  const access = buildAccessMap(roles, permission, grantsByRole);
  const out: Record<string, number> = {};
  for (const role of roles) {
    const verdict = access[role];
    out[role] = verdict === 'redirect_login' ? 401 : verdict === 'allowed' ? okStatus : 403;
  }
  return out;
}

/** Project discovered controller actions into the plan's `endpoints[]`. */
export function projectEndpoints(
  endpoints: readonly DiscoveredEndpoint[],
  roles: readonly string[],
  grantsByRole: Readonly<Record<string, readonly { path: string; isWildcard: boolean }[]>>,
): Endpoint[] {
  return endpoints.map((e) => {
    const ok = e.okStatus ?? okStatusFor(e.method);
    // [AllowAnonymous] => no auth gate: every role (incl. anonymous) gets the success status.
    const expected_by_role = e.allowAnonymous
      ? Object.fromEntries(roles.map((r) => [r, ok]))
      : expectedStatusByRole(roles, e.permission, grantsByRole, ok);
    const endpoint: Endpoint = {
      id: `${e.method.toLowerCase()}:${endpointIdPath(e.route)}`,
      method: e.method,
      route: e.route,
      expected_by_role,
    };
    if (e.controller) endpoint.controller = e.controller;
    if (e.permission) endpoint.permission = e.permission;
    return endpoint;
  });
}
