import { access, mkdir, readdir } from "node:fs/promises";
import { join } from "node:path";
import { z } from "zod";
import {
  contractCodec,
  parseContract,
  type ContractEnvelope,
} from "../../../schema/src/decisions/contract.js";
import type { DetectorVersionRef } from "../../../schema/src/decisions/scope.js";
import {
  defineRecordCodec,
  type RecordCodec,
} from "../../../schema/src/records/envelope.js";
import { atomicWriteRecord, CorruptStateError, readRecord } from "../store/atomic.js";
import { storeLayout } from "../store/layout.js";
import type { DraftContract } from "./promotion.js";

export const CONTRACT_INDEX_VERSION = "contract-index/v1" as const;
export const CALIBRATION_RECORD_VERSION = "contract-calibration/v1" as const;

const contractIndexSchema = z
  .object({
    schemaVersion: z.literal(CONTRACT_INDEX_VERSION),
    activeIds: z.array(z.string()),
  })
  .strict();

export type ContractStoreIndex = z.infer<typeof contractIndexSchema>;

export const contractIndexCodec: RecordCodec<ContractStoreIndex> = defineRecordCodec({
  schema: contractIndexSchema,
  currentVersion: CONTRACT_INDEX_VERSION,
});

const calibrationRecordSchema = z
  .object({
    schemaVersion: z.literal(CALIBRATION_RECORD_VERSION),
    contractId: z.string(),
    contractVersion: z.string(),
    evidenceRef: z
      .object({
        relativePath: z.string(),
        contentHash: z.string(),
      })
      .strict(),
    passed: z.boolean(),
    calibratedAt: z.string(),
    witnessHash: z.string(),
  })
  .strict();

export type CalibrationRecord = z.infer<typeof calibrationRecordSchema>;

export const calibrationRecordCodec: RecordCodec<CalibrationRecord> = defineRecordCodec({
  schema: calibrationRecordSchema,
  currentVersion: CALIBRATION_RECORD_VERSION,
});

export type ContractStorageKind = "confirmed" | "inferred";

export class ContractAuthorityError extends Error {
  readonly contractId: string;
  readonly contractVersion: string;
  readonly reason: string;

  constructor(contractId: string, contractVersion: string, reason: string) {
    super(
      `Contract authority unavailable for ${contractId}@${contractVersion}: ${reason}`,
    );
    this.name = "ContractAuthorityError";
    this.contractId = contractId;
    this.contractVersion = contractVersion;
    this.reason = reason;
  }
}

function compareUnicodeScalars(a: string, b: string): number {
  let i = 0;
  let j = 0;

  while (i < a.length && j < b.length) {
    const codeA = a.codePointAt(i);
    const codeB = b.codePointAt(j);
    if (codeA === undefined || codeB === undefined) {
      break;
    }
    if (codeA !== codeB) {
      return codeA < codeB ? -1 : 1;
    }
    i += codeA > 0xffff ? 2 : 1;
    j += codeB > 0xffff ? 2 : 1;
  }

  return a.length - b.length;
}

function confirmedContractPath(root: string, contractId: string): string {
  return join(storeLayout(root).contractsDir(), "confirmed", `${contractId}.json`);
}

function inferredContractPath(root: string, contractId: string): string {
  return join(storeLayout(root).contractsDir(), "inferred", `${contractId}.json`);
}

function contractPath(
  root: string,
  contractId: string,
  kind: ContractStorageKind,
): string {
  return kind === "confirmed"
    ? confirmedContractPath(root, contractId)
    : inferredContractPath(root, contractId);
}

function calibrationRecordPath(root: string, contractId: string): string {
  return join(storeLayout(root).contractsDir(), "calibration", `${contractId}.json`);
}

function oracleManifestPath(root: string, detector: DetectorVersionRef): string {
  return join(
    storeLayout(root).config(),
    "oracles",
    `${detector.id}@${detector.version}.json`,
  );
}

function contractIndexPath(root: string): string {
  return join(storeLayout(root).contractsDir(), "store-index.json");
}

function isContractRecordFile(fileName: string): boolean {
  return fileName.endsWith(".json") && fileName !== "store-index.json";
}

export class ContractStore {
  constructor(
    private readonly index: ContractStoreIndex,
    private readonly contracts: Map<string, ContractEnvelope>,
    private readonly storageKinds: Map<string, ContractStorageKind>,
  ) {}

  getContract(id: string): ContractEnvelope | undefined {
    return this.contracts.get(id);
  }

  getStorageKind(id: string): ContractStorageKind | undefined {
    return this.storageKinds.get(id);
  }

  isActive(id: string): boolean {
    return this.index.activeIds.includes(id);
  }

  activeIds(): readonly string[] {
    return this.index.activeIds;
  }

  confirmedContracts(): ContractEnvelope[] {
    return [...this.contracts.entries()]
      .filter(([, contract]) => this.storageKinds.get(contract.id) === "confirmed")
      .map(([, contract]) => contract)
      .sort((left, right) => compareUnicodeScalars(left.id, right.id));
  }

  inferredContracts(): ContractEnvelope[] {
    return [...this.contracts.entries()]
      .filter(([, contract]) => this.storageKinds.get(contract.id) === "inferred")
      .map(([, contract]) => contract)
      .sort((left, right) => compareUnicodeScalars(left.id, right.id));
  }
}

export async function readContractIndex(root: string): Promise<ContractStoreIndex> {
  const indexPath = contractIndexPath(root);
  try {
    return await readRecord(indexPath, contractIndexCodec, { stateClass: "blocking" });
  } catch (error) {
    if (
      error instanceof CorruptStateError &&
      error.reason === "record file is missing"
    ) {
      return {
        schemaVersion: CONTRACT_INDEX_VERSION,
        activeIds: [],
      };
    }
    throw error;
  }
}

async function listContractIds(
  root: string,
  kind: ContractStorageKind,
): Promise<string[]> {
  const directory = join(
    storeLayout(root).contractsDir(),
    kind === "confirmed" ? "confirmed" : "inferred",
  );

  let entries: string[];
  try {
    entries = await readdir(directory);
  } catch (error) {
    if ((error as NodeJS.ErrnoException).code === "ENOENT") {
      return [];
    }
    throw error;
  }

  return entries
    .filter(isContractRecordFile)
    .map((fileName) => fileName.slice(0, -".json".length))
    .sort(compareUnicodeScalars);
}

export async function readContractRecord(
  root: string,
  contractId: string,
  kind: ContractStorageKind,
): Promise<ContractEnvelope> {
  const path = contractPath(root, contractId, kind);
  return readRecord(path, contractCodec, { stateClass: "blocking" });
}

export async function loadContractStore(root: string): Promise<ContractStore> {
  const index = await readContractIndex(root);
  const contracts = new Map<string, ContractEnvelope>();
  const storageKinds = new Map<string, ContractStorageKind>();

  for (const kind of ["confirmed", "inferred"] as const) {
    const contractIds = await listContractIds(root, kind);
    for (const contractId of contractIds) {
      const contract = await readContractRecord(root, contractId, kind);
      contracts.set(contractId, contract);
      storageKinds.set(contractId, kind);
    }
  }

  return new ContractStore(index, contracts, storageKinds);
}

export async function writeContractIndex(
  root: string,
  index: ContractStoreIndex,
): Promise<void> {
  await atomicWriteRecord(contractIndexPath(root), index, contractIndexCodec);
}

export async function persistContractRecord(
  root: string,
  contract: ContractEnvelope | DraftContract,
  options: { kind?: ContractStorageKind } = {},
): Promise<void> {
  const kind = options.kind ?? contract.kind;
  const parsed = parseContract(contract);
  const path = contractPath(root, parsed.id, kind);
  await mkdir(join(storeLayout(root).contractsDir(), kind), { recursive: true });
  await atomicWriteRecord(path, parsed, contractCodec);

  if (parsed.status === "active" && kind === "confirmed") {
    const index = await readContractIndex(root);
    const activeIds = new Set(index.activeIds);
    activeIds.add(parsed.id);
    await writeContractIndex(root, {
      schemaVersion: CONTRACT_INDEX_VERSION,
      activeIds: [...activeIds].sort(compareUnicodeScalars),
    });
  }
}

export async function writeCalibrationRecord(
  root: string,
  record: Omit<CalibrationRecord, "schemaVersion">,
): Promise<void> {
  const path = calibrationRecordPath(root, record.contractId);
  await mkdir(join(storeLayout(root).contractsDir(), "calibration"), { recursive: true });
  await atomicWriteRecord(
    path,
    {
      schemaVersion: CALIBRATION_RECORD_VERSION,
      ...record,
    },
    calibrationRecordCodec,
  );
}

export async function readCalibrationRecord(
  root: string,
  contractId: string,
): Promise<CalibrationRecord | undefined> {
  const path = calibrationRecordPath(root, contractId);
  try {
    return await readRecord(path, calibrationRecordCodec, { stateClass: "blocking" });
  } catch (error) {
    if (
      error instanceof CorruptStateError &&
      error.reason === "record file is missing"
    ) {
      return undefined;
    }
    throw error;
  }
}

export async function writeOracleManifest(
  root: string,
  detector: DetectorVersionRef,
): Promise<void> {
  const path = oracleManifestPath(root, detector);
  await mkdir(join(storeLayout(root).config(), "oracles"), { recursive: true });
  await atomicWriteRecord(
    path,
    {
      schemaVersion: "oracle-manifest/v1",
      detector,
      loadable: true,
    },
    defineRecordCodec({
      schema: z
        .object({
          schemaVersion: z.literal("oracle-manifest/v1"),
          detector: z
            .object({
              id: z.string(),
              version: z.string(),
            })
            .strict(),
          loadable: z.literal(true),
        })
        .strict(),
      currentVersion: "oracle-manifest/v1",
    }),
  );
}

async function isOracleLoadable(
  root: string,
  detector: DetectorVersionRef,
): Promise<boolean> {
  const path = oracleManifestPath(root, detector);
  try {
    await access(path);
    return true;
  } catch {
    return false;
  }
}

async function isCalibrationEvidenceLoadable(
  root: string,
  relativePath: string,
): Promise<boolean> {
  const absolutePath = join(root, relativePath);
  try {
    await access(absolutePath);
    return true;
  } catch {
    return false;
  }
}

export async function loadAuthoritativeContract(
  root: string,
  contractId: string,
  contractVersion: string,
): Promise<ContractEnvelope> {
  const store = await loadContractStore(root);
  const contract = store.getContract(contractId);
  if (contract === undefined) {
    throw new ContractAuthorityError(contractId, contractVersion, "contract record is missing");
  }

  if (store.getStorageKind(contractId) === "inferred") {
    throw new ContractAuthorityError(
      contractId,
      contractVersion,
      "inferred contracts are not blocking-authoritative",
    );
  }

  if (contract.kind !== "confirmed") {
    throw new ContractAuthorityError(
      contractId,
      contractVersion,
      "only confirmed contracts provide blocking authority",
    );
  }

  if (contract.status !== "active") {
    throw new ContractAuthorityError(
      contractId,
      contractVersion,
      "contract is not active",
    );
  }

  if (contract.version !== contractVersion) {
    throw new ContractAuthorityError(
      contractId,
      contractVersion,
      "requested contract version does not match stored version",
    );
  }

  const oracleLoadable = await isOracleLoadable(root, contract.oracleRef.detector);
  if (!oracleLoadable) {
    throw new ContractAuthorityError(
      contractId,
      contractVersion,
      "executable oracle reference is not loadable",
    );
  }

  const calibration = await readCalibrationRecord(root, contractId);
  if (calibration === undefined) {
    throw new ContractAuthorityError(
      contractId,
      contractVersion,
      "calibration record is missing",
    );
  }

  if (!calibration.passed) {
    throw new ContractAuthorityError(
      contractId,
      contractVersion,
      "calibration evidence did not pass",
    );
  }

  if (
    calibration.contractVersion !== contract.version ||
    calibration.evidenceRef.relativePath !== contract.calibrationEvidenceRef.relativePath ||
    calibration.evidenceRef.contentHash !== contract.calibrationEvidenceRef.contentHash
  ) {
    throw new ContractAuthorityError(
      contractId,
      contractVersion,
      "calibration record does not match contract evidence reference",
    );
  }

  const evidenceLoadable = await isCalibrationEvidenceLoadable(
    root,
    contract.calibrationEvidenceRef.relativePath,
  );
  if (!evidenceLoadable) {
    throw new ContractAuthorityError(
      contractId,
      contractVersion,
      "calibration evidence is not loadable",
    );
  }

  return contract;
}
