import { constants } from 'node:fs';
import { access, stat } from 'node:fs/promises';
import { join, resolve } from 'node:path';

import { createSpawnProcessAdapter } from './adapter.js';
import { builtInDefinitions, CHECK_SET_POLICY, checkSetForKind } from './definitions.js';
import type {
  AggregateState,
  CheckResult,
  CheckStatus,
  EvaluateOptions,
  EvaluationReport,
  ProcessAdapter,
  Subject,
  SubjectKind,
} from './types.js';
import { detectProjectKind } from '../detect.js';

async function pathExists(targetPath: string): Promise<boolean> {
  try {
    await access(targetPath, constants.F_OK);
    return true;
  } catch {
    return false;
  }
}

async function isDirectory(targetPath: string): Promise<boolean> {
  try {
    return (await stat(targetPath)).isDirectory();
  } catch {
    return false;
  }
}

/**
 * Subject-kind detection for the evaluator. Extends the scaffold detection
 * (detect.ts) with the `monorepo` kind: a pnpm workspace is this repository's
 * own subject shape and carries a different rails file set (probe-inventory
 * gap 7 — check sets are per subject, not one global file list).
 */
export async function detectSubjectKind(subjectPath: string): Promise<SubjectKind> {
  if (await pathExists(join(subjectPath, 'pnpm-workspace.yaml'))) {
    return 'monorepo';
  }
  const kind = await detectProjectKind(subjectPath);
  return kind;
}

/**
 * Aggregate state, MACP-style discipline: `passed` only when at least one
 * check produced a verdict AND every verdict is `passed` or an explicitly
 * qualified `not-applicable`. Precedence is fail-closed: error > blocked >
 * failed > passed; an empty result list aggregates to `blocked`.
 */
export function aggregateState(results: readonly CheckResult[]): AggregateState {
  if (results.length === 0) {
    return 'blocked';
  }
  const has = (status: CheckStatus): boolean => results.some((result) => result.status === status);
  if (has('error')) {
    return 'error';
  }
  if (has('blocked')) {
    return 'blocked';
  }
  if (has('failed')) {
    return 'failed';
  }
  return 'passed';
}

function reasonFrom(error: unknown): string {
  return error instanceof Error ? error.message : String(error);
}

/**
 * Evaluate one subject against a set of checks, producing typed verdicts.
 *
 * Fail-closed invariants (RI-N4):
 * - unknown check id            → `error` (never passed)
 * - subject directory absent    → every verdict `blocked`
 * - check implementation threw  → `error`
 * - non-passed without a reason → `error` (no unqualified skips)
 * - check not applicable        → `not-applicable` WITH a reason
 */
export async function evaluateSubject(options: EvaluateOptions): Promise<EvaluationReport> {
  const subjectPath = resolve(options.subjectPath);
  const subject: Subject = {
    path: subjectPath,
    kind: await detectSubjectKind(subjectPath),
  };

  const definitions = options.definitions ?? builtInDefinitions();
  const byId = new Map(definitions.map((definition) => [definition.id, definition]));
  const requested = options.checkIds ?? checkSetForKind(subject.kind);
  const adapter: ProcessAdapter = options.adapter ?? createSpawnProcessAdapter();

  const results: CheckResult[] = [];
  const definitionDigests: Record<string, string> = {};

  for (const checkId of requested) {
    const definition = byId.get(checkId);
    if (definition === undefined) {
      const known = definitions.map((entry) => entry.id).join(', ');
      results.push({
        status: 'error',
        checkId,
        checkVersion: 'unknown',
        subject: subjectPath,
        reason: `unknown check id '${checkId}' — no registered definition (known: ${known})`,
      });
      continue;
    }

    definitionDigests[checkId] = definition.definitionDigest;

    if (!(await isDirectory(subjectPath))) {
      results.push({
        status: 'blocked',
        checkId,
        checkVersion: definition.version,
        subject: subjectPath,
        reason: `subject directory does not exist: ${subjectPath}`,
      });
      continue;
    }

    if (!definition.appliesTo.includes(subject.kind)) {
      results.push({
        status: 'not-applicable',
        checkId,
        checkVersion: definition.version,
        subject: subjectPath,
        reason: `check '${checkId}' does not apply to subject kind '${subject.kind}'`,
      });
      continue;
    }

    try {
      const inputs = options.inputs?.[checkId] ?? {};
      const outcome = await definition.evaluate({
        subject,
        params: definition.params,
        inputs,
        adapter,
      });
      if (outcome.status !== 'passed' && (outcome.reason === undefined || outcome.reason === '')) {
        results.push({
          status: 'error',
          checkId,
          checkVersion: definition.version,
          subject: subjectPath,
          reason: `check returned status '${outcome.status}' without a reason — treated as error`,
        });
        continue;
      }
      results.push({
        status: outcome.status,
        checkId,
        checkVersion: definition.version,
        subject: subjectPath,
        reason: outcome.reason,
      });
    } catch (error) {
      results.push({
        status: 'error',
        checkId,
        checkVersion: definition.version,
        subject: subjectPath,
        reason: `check implementation threw: ${reasonFrom(error)}`,
      });
    }
  }

  return {
    subject,
    results,
    definitionDigests,
    checkSetVersion: CHECK_SET_POLICY.version,
    state: aggregateState(results),
  };
}
