import { INestApplication, Logger } from '@nestjs/common';
import { ACCESS_CONTRACT_REGISTRY, AccessContractRow } from './access-contract.registry';
import { ACCESS_CONTRACT_EXCEPTIONS, AccessContractException } from './access-contract.exceptions';

// 단언 모드 (계획 Task 6(4)). 코드 상수이고 env 가 아니다. 첫 릴리즈는 warn 이고, fail 전환은 사용자 한 줄 커밋이다.
export type AccessContractMode = 'warn' | 'fail';
export const ACCESS_CONTRACT_MODE: AccessContractMode = 'warn';

export const RULE_UNDECLARED = '선언 없음';
export const RULE_UNKNOWN_PATH = '미지 경로';
export const RULE_STALE = '런타임 부재';
export const RULE_DUPLICATE = '중복';
export const RULE_UNASSERTABLE = '단언 불능';

export interface RuntimeRoute {
  readonly method: string;
  readonly path: string;
}

export interface AccessContractViolation {
  readonly rule: string;
  readonly method?: string;
  readonly path?: string;
  readonly detail?: string;
}

const keyOf = (method: string, path: string) => `${method} ${path}`;

// 예외는 세 검사 모두에서 뺀다 (계획 Task 6(1)). 그래서 예외 경로가 런타임에 없어도 위반이 아니다.
export function isException(path: string, exceptions: readonly AccessContractException[]): boolean {
  return exceptions.some((e) => ('path' in e ? e.path === path : path.startsWith(e.prefix)));
}

// 순수 판정. 행 누락 · 미지 경로 · 중복을 위반으로 낸다. 입력 배열은 바꾸지 않는다.
export function checkAccessContracts(
  runtimeRoutes: readonly RuntimeRoute[],
  registry: readonly Pick<AccessContractRow, 'method' | 'path'>[],
  exceptions: readonly AccessContractException[] = ACCESS_CONTRACT_EXCEPTIONS,
): AccessContractViolation[] {
  const violations: AccessContractViolation[] = [];
  const declared = new Set<string>();
  const declaredPaths = new Set<string>();
  for (const row of registry) {
    const k = keyOf(row.method, row.path);
    if (declared.has(k)) {
      if (!isException(row.path, exceptions)) violations.push({ rule: RULE_DUPLICATE, method: row.method, path: row.path, detail: '레지스트리' });
      continue;
    }
    declared.add(k);
    declaredPaths.add(row.path);
  }
  const served = new Set<string>();
  for (const route of runtimeRoutes) {
    if (isException(route.path, exceptions)) continue;
    const k = keyOf(route.method, route.path);
    if (served.has(k)) {
      violations.push({ rule: RULE_DUPLICATE, method: route.method, path: route.path, detail: '런타임' });
      continue;
    }
    served.add(k);
    if (!declared.has(k)) {
      const rule = declaredPaths.has(route.path) ? RULE_UNDECLARED : RULE_UNKNOWN_PATH;
      violations.push({ rule, method: route.method, path: route.path });
    }
  }
  for (const k of declared) {
    if (served.has(k)) continue;
    const sp = k.indexOf(' ');
    const method = k.slice(0, sp);
    const path = k.slice(sp + 1);
    if (isException(path, exceptions)) continue;
    violations.push({ rule: RULE_STALE, method, path });
  }
  return violations;
}

// 단언 불능 (계획 Task 6(3)). 라우터 스택이 없거나 수집이 0건이면 위반 1건으로 센다. 모드와 무관하다.
export function judgeRuntimeRoutes(
  routes: readonly RuntimeRoute[] | null,
  registry: readonly Pick<AccessContractRow, 'method' | 'path'>[] = ACCESS_CONTRACT_REGISTRY,
  exceptions: readonly AccessContractException[] = ACCESS_CONTRACT_EXCEPTIONS,
): AccessContractViolation[] {
  if (routes === null) return [{ rule: RULE_UNASSERTABLE, detail: '라우터 스택 없음' }];
  if (routes.length === 0) return [{ rule: RULE_UNASSERTABLE, detail: '수집 0건' }];
  return checkAccessContracts(routes, registry, exceptions);
}

// Express 라우터 스택의 route 항목을 메서드 · 경로로 모은다. 와일드카드(*) 등록은 뺀다 (express-wildcard 규칙).
// init 은 listen 이 부르는 것과 같은 객체라 두 번째 호출은 아무 일도 하지 않는다.
export async function collectExpressRoutes(app: INestApplication): Promise<RuntimeRoute[] | null> {
  await app.init();
  const server = app.getHttpAdapter().getInstance();
  const stack: unknown = server?._router?.stack ?? server?.router?.stack;
  if (!Array.isArray(stack)) return null;
  const routes: RuntimeRoute[] = [];
  for (const layer of stack) {
    const route = layer?.route;
    if (!route) continue;
    if (String(route.path).includes('*')) continue;
    for (const m of Object.keys(route.methods)) {
      if (!route.methods[m]) continue;
      routes.push({ method: m === '_all' ? 'ALL' : m.toUpperCase(), path: String(route.path) });
    }
  }
  return routes;
}

// 모드별 처리. warn 은 위반 개수와 목록을 Logger.warn 한 번으로 내고 계속한다. fail 은 던진다.
// 로그에는 규칙명 · 메서드 · 경로만 싣는다.
export function enforceAccessContracts(
  violations: readonly AccessContractViolation[],
  mode: AccessContractMode = ACCESS_CONTRACT_MODE,
  logger: Pick<Logger, 'warn'> = new Logger('AccessContract'),
): void {
  if (violations.length === 0) return;
  const list = violations
    .map((v) => (v.method ? `${v.rule} ${v.method} ${v.path}` : `${v.rule} ${v.detail ?? ''}`.trim()))
    .join('; ');
  const message = `[access-contract] 위반 ${violations.length}건 (mode=${mode}): ${list}`;
  if (mode === 'fail') throw new Error(message);
  logger.warn(message);
}

// 부팅 경로의 단언 (main.ts 가 listen 앞에서 한 번 부른다). init 이 던지면 그대로 던진다 (부팅 실패는 원래 실패다).
// 비교 함수가 던지면 단언 불능 위반 1건으로 센다.
export async function assertAccessContracts(app: INestApplication, mode: AccessContractMode = ACCESS_CONTRACT_MODE): Promise<void> {
  const routes = await collectExpressRoutes(app);
  let violations: AccessContractViolation[];
  try {
    violations = judgeRuntimeRoutes(routes);
  } catch (e) {
    violations = [{ rule: RULE_UNASSERTABLE, detail: `비교 실패 (${e instanceof Error ? e.name : 'unknown'})` }];
  }
  enforceAccessContracts(violations, mode);
}
