import { mkdtemp } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import type { ExecutionContext } from "../../../schema/src/records/context.js";
import {
  classify,
  type ClassifiedRun,
  type DetectorOutcome,
  type KernelStores,
} from "../../../core/src/classify/classifier.js";
import { loadContractStore } from "../../../core/src/contracts/store.js";
import { loadDecisionStore } from "../../../core/src/decisions/store.js";
import type {
  ObligationClosureResult,
  ObligationClosureSpec,
  ObligationViolation,
} from "./types.js";

const OBLIGATION_DETECTOR = { id: "async-obligation-closure", version: "1.0.0" } as const;

function assertNonEmptyString(value: string, field: string): void {
  if (value.trim().length === 0) {
    throw new Error(`${field} must be a non-empty string`);
  }
}

function isSuccessStatus(status: string): boolean {
  const normalized = status.trim().toLowerCase();
  return (
    normalized === "success" ||
    normalized === "succeeded" ||
    normalized === "completed" ||
    normalized === "ok" ||
    normalized === "done"
  );
}

function obligationContext(branchId: string): ExecutionContext {
  return {
    kind: "api",
    surfaceId: branchId,
    adapterId: "async-obligation-closure",
    environment: {},
    seed: branchId,
  };
}

async function createDefaultStores(now = "2026-07-25T12:00:00.000Z"): Promise<KernelStores> {
  const root = await mkdtemp(join(tmpdir(), "invariantum-async-obligation-"));
  const [decisions, contracts] = await Promise.all([
    loadDecisionStore(root),
    loadContractStore(root),
  ]);
  return {
    decisions,
    contracts,
    authoritativeContracts: new Set<string>(),
    now,
  };
}

function detectViolations(spec: ObligationClosureSpec): ObligationViolation[] {
  const violations: ObligationViolation[] = [];

  if (!isSuccessStatus(spec.claimedStatus)) {
    return violations;
  }

  if (spec.ownershipViolations !== undefined && spec.ownershipViolations.length > 0) {
    for (const fact of spec.ownershipViolations) {
      violations.push({
        rule: "UNI-024",
        fact,
        details: { kind: "ownership-or-atomicity" },
      });
    }
  }

  const representations = spec.representations;
  if (representations !== undefined) {
    const values = [
      representations.claimed,
      representations.durable,
      representations.visible,
    ].filter((value): value is string => value !== undefined);
    const normalized = new Set(values.map((value) => value.trim().toLowerCase()));
    if (normalized.size > 1) {
      violations.push({
        rule: "UNI-020",
        fact: "Same-semantic representations disagree after normalization",
        details: { representations },
      });
    }
  }

  const derived = spec.derivedValues;
  if (derived !== undefined && derived.left + derived.right !== derived.total) {
    violations.push({
      rule: "UNI-021",
      fact: "Derived-value relation fails closure",
      details: { derived },
    });
  }

  const outstandingConfirmed = spec.obligations.filter(
    (obligation) => obligation.mandatory && !obligation.completed && obligation.status === "confirmed",
  );
  if (outstandingConfirmed.length > 0) {
    violations.push({
      rule: "UNI-022",
      fact: "Terminal success contradicts outstanding confirmed mandatory completion obligation",
      details: { outstanding: outstandingConfirmed },
    });
  }

  const durableDisagrees =
    spec.durableOutcome !== undefined &&
    spec.durableOutcome.status.trim().toLowerCase() !== "success";
  const sideEffectMissing =
    spec.observableSideEffect !== undefined && !spec.observableSideEffect.observed;

  if (durableDisagrees || sideEffectMissing) {
    violations.push({
      rule: "UNI-023",
      fact: "Claimed success without durable agreement or observable completion",
      details: {
        durableOutcome: spec.durableOutcome,
        observableSideEffect: spec.observableSideEffect,
      },
    });
  }

  const outstandingInferred = spec.obligations.filter(
    (obligation) => obligation.mandatory && !obligation.completed && obligation.status === "inferred",
  );
  if (outstandingInferred.length > 0) {
    violations.push({
      rule: "UNI-024",
      fact: "Outstanding inferred mandatory obligation remains unproven",
      details: { outstanding: outstandingInferred },
    });
  }

  return violations;
}

function violationLaneEligibility(violation: ObligationViolation): "advisory" | "blocking-eligible" {
  if (violation.rule === "UNI-024" && violation.details !== undefined) {
    const details = violation.details as { outstanding?: Array<{ status?: string }> };
    const outstanding = details.outstanding ?? [];
    if (outstanding.every((entry) => entry.status === "inferred")) {
      return "advisory";
    }
  }
  if (violation.rule === "UNI-022") {
    return "blocking-eligible";
  }
  if (violation.rule === "UNI-023") {
    return "blocking-eligible";
  }
  if (violation.rule === "UNI-020" || violation.rule === "UNI-021") {
    return "blocking-eligible";
  }
  if (violation.details !== undefined) {
    const details = violation.details as { kind?: string };
    if (details.kind === "ownership-or-atomicity") {
      return "blocking-eligible";
    }
  }
  return "blocking-eligible";
}

function buildViolationOutcome(
  spec: ObligationClosureSpec,
  violation: ObligationViolation,
  laneEligibility: "advisory" | "blocking-eligible",
): DetectorOutcome {
  const context = obligationContext(spec.branchId);
  return {
    detector: OBLIGATION_DETECTOR,
    class: "async-obligation-violation",
    severity: laneEligibility === "blocking-eligible" ? "high" : "medium",
    target: { kind: "journey-branch", canonical: spec.branchId },
    context,
    summary: `${violation.rule}: ${violation.fact}`,
    evidence: [
      {
        truthSource: laneEligibility === "blocking-eligible" ? "universal" : "inferred",
        payload: violation,
      },
    ],
    artifacts: [],
    laneEligibility,
    ...(laneEligibility === "blocking-eligible" ? { proofConditionMet: true } : {}),
    scope: {
      id: `async-obligation:${spec.branchId}:${violation.rule}`,
      detectorId: OBLIGATION_DETECTOR.id,
      surfaceId: spec.branchId,
    },
    violation,
    contractOrConfig: { branchId: spec.branchId, claimedStatus: spec.claimedStatus },
    contextDimensions: { rule: violation.rule },
  };
}

async function classifyViolations(
  spec: ObligationClosureSpec,
  violations: ObligationViolation[],
  runId: string,
): Promise<ClassifiedRun> {
  const detectorOutcomes = violations.map((violation) =>
    buildViolationOutcome(spec, violation, violationLaneEligibility(violation)),
  );
  const stores = await createDefaultStores();
  return classify({
    detectorOutcomes,
    harnessEvents: [],
    coverageEvents: [],
    stores,
    runId,
  });
}

function resolveLaneEligibility(
  violations: ObligationViolation[],
): "advisory" | "blocking-eligible" {
  if (violations.length === 0) {
    return "advisory";
  }
  const hasBlocking = violations.some(
    (violation) => violationLaneEligibility(violation) === "blocking-eligible",
  );
  return hasBlocking ? "blocking-eligible" : "advisory";
}

export async function evaluateObligationClosure(
  spec: ObligationClosureSpec,
  options: { runId?: string } = {},
): Promise<ObligationClosureResult> {
  assertNonEmptyString(spec.branchId, "branchId");
  assertNonEmptyString(spec.claimedStatus, "claimedStatus");
  const runId = options.runId ?? `async-obligation:${spec.branchId}`;

  const violations = detectViolations(spec);
  const classified = await classifyViolations(spec, violations, runId);
  const laneEligibility = resolveLaneEligibility(violations);

  return {
    holds: violations.length === 0,
    violations,
    laneEligibility,
    classified,
  };
}
