import type BetterSqlite3 from "better-sqlite3";

import { ModelConfigurationService, modelIds, type ModelConfigCandidate } from "./model-configuration.js";

const safeRefPattern = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,159}$/;
const requiredRatios = ["3:4", "1:1", "4:3", "9:16"];
const requiredAdapterErrors = [
  "upstream_timeout", "upstream_failed", "safety_rejected", "gateway_balance_insufficient",
  "gateway_contract_invalid", "reference_invalid", "unknown_retryable", "unknown_non_retryable",
];
const forbiddenKeyFragments = ["credential", "password", "prompt_text", "raw", "secret", "token"];

export interface ModelContractEvidenceInput {
  evidence_hash: string;
  evidence_ref: string;
  matrix: unknown;
  model_id: string;
  verified_at: string;
  verifier_ref: string;
}

function hasForbiddenEvidenceKey(value: unknown, depth = 0): boolean {
  if (depth > 8 || !value || typeof value !== "object") return false;
  if (Array.isArray(value)) return value.some((entry) => hasForbiddenEvidenceKey(entry, depth + 1));
  return Object.entries(value).some(([key, entry]) => (
    forbiddenKeyFragments.some((fragment) => key.toLowerCase().includes(fragment))
    || hasForbiddenEvidenceKey(entry, depth + 1)
  ));
}

function validateEvidence(input: ModelContractEvidenceInput) {
  if (!modelIds.includes(input.model_id as typeof modelIds[number])) throw new Error("contract_evidence_model_invalid");
  if (!safeRefPattern.test(input.evidence_hash) || !safeRefPattern.test(input.evidence_ref) || !safeRefPattern.test(input.verifier_ref)) {
    throw new Error("contract_evidence_reference_invalid");
  }
  const verifiedAt = Date.parse(input.verified_at);
  if (!Number.isFinite(verifiedAt) || new Date(verifiedAt).toISOString() !== input.verified_at) throw new Error("contract_evidence_time_invalid");
  if (hasForbiddenEvidenceKey(input.matrix)) throw new Error("contract_evidence_sensitive_field");
  if (!input.matrix || typeof input.matrix !== "object") throw new Error("contract_evidence_matrix_incomplete");
  const matrix = input.matrix as Record<string, unknown>;
  const passedSingleOutput = (value: unknown) => Boolean(value && typeof value === "object"
    && "status" in value && value.status === "passed" && "outputs" in value && value.outputs === 1);
  const ratios = Array.isArray(matrix.ratios) ? matrix.ratios as Array<Record<string, unknown>> : [];
  const ratioNames = ratios.filter(passedSingleOutput).map((entry) => entry.ratio).toSorted();
  const executionModes = Array.isArray(matrix.execution_modes) ? matrix.execution_modes : [];
  const errorMapping = Array.isArray(matrix.error_mapping) ? matrix.error_mapping : [];
  const executionComplete = executionModes.includes("sync") || (executionModes.includes("async") && executionModes.includes("poll"));
  if (matrix.model_id !== input.model_id || !passedSingleOutput(matrix.pure_text) || !passedSingleOutput(matrix.reference_image)
    || JSON.stringify(ratioNames) !== JSON.stringify([...requiredRatios].toSorted()) || !executionComplete
    || !requiredAdapterErrors.every((category) => errorMapping.includes(category))) {
    throw new Error("contract_evidence_matrix_incomplete");
  }
  return verifiedAt;
}

function editableCandidates(models: ReturnType<ModelConfigurationService["read"]>["models"]): ModelConfigCandidate[] {
  return models.map(({ config_version: _configVersion, runtime_availability: _runtime, ...candidate }) => structuredClone(candidate));
}

export class ModelContractEvidenceService {
  readonly database: BetterSqlite3.Database;
  private readonly clock: () => number;
  private readonly models: ModelConfigurationService;

  constructor(input: { clock?: () => number; database: BetterSqlite3.Database; models: ModelConfigurationService }) {
    this.clock = input.clock ?? Date.now;
    this.database = input.database;
    this.models = input.models;
    this.migrate();
  }

  recordVerified(input: {
    actorId: string;
    evidence: ModelContractEvidenceInput;
    expectedConfigSetVersion: number;
    idempotencyKey: string;
  }) {
    const verifiedAt = validateEvidence(input.evidence);
    return this.database.transaction(() => {
      const current = this.models.read();
      if (current.config_set_version !== input.expectedConfigSetVersion) throw new Error("contract_evidence_config_set_conflict");
      const existingHash = this.database.prepare("SELECT model_id FROM model_contract_evidence WHERE evidence_hash = ?").get(input.evidence.evidence_hash) as { model_id: string } | undefined;
      if (existingHash && existingHash.model_id !== input.evidence.model_id) throw new Error("contract_evidence_shared_between_models");
      const candidates = editableCandidates(current.models);
      const target = candidates.find((model) => model.model_id === input.evidence.model_id)!;
      target.contract_validation_status = "verified";
      target.contract_evidence_ref = input.evidence.evidence_ref;
      const configuration = this.models.replace({
        actorId: input.actorId,
        expectedConfigSetVersion: input.expectedConfigSetVersion,
        idempotencyKey: input.idempotencyKey,
        models: candidates,
      });
      const model = configuration.models.find((candidate) => candidate.model_id === input.evidence.model_id)!;
      this.database.prepare(`
        INSERT OR IGNORE INTO model_contract_evidence (
          model_id, config_version, evidence_hash, evidence_ref, verifier_ref, verified_at, evidence_json, created_at
        ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
      `).run(
        input.evidence.model_id, model.config_version, input.evidence.evidence_hash, input.evidence.evidence_ref,
        input.evidence.verifier_ref, verifiedAt, JSON.stringify(input.evidence.matrix), this.clock(),
      );
      return { configuration, evidence: structuredClone(input.evidence), model };
    }).immediate();
  }

  read(modelId: string, configVersion: number) {
    return this.database.prepare(`
      SELECT model_id, config_version, evidence_hash, evidence_ref, verifier_ref, verified_at, evidence_json
      FROM model_contract_evidence WHERE model_id = ? AND config_version = ?
    `).get(modelId, configVersion) as {
      config_version: number; evidence_hash: string; evidence_json: string; evidence_ref: string;
      model_id: string; verified_at: number; verifier_ref: string;
    } | undefined;
  }

  private migrate() {
    this.database.exec(`
      CREATE TABLE IF NOT EXISTS model_contract_evidence (
        model_id TEXT NOT NULL,
        config_version INTEGER NOT NULL,
        evidence_hash TEXT NOT NULL UNIQUE,
        evidence_ref TEXT NOT NULL,
        verifier_ref TEXT NOT NULL,
        verified_at INTEGER NOT NULL,
        evidence_json TEXT NOT NULL CHECK (json_valid(evidence_json)),
        created_at INTEGER NOT NULL,
        PRIMARY KEY (model_id, config_version),
        FOREIGN KEY (model_id, config_version) REFERENCES model_config_versions(model_id, config_version)
      );
      CREATE TRIGGER IF NOT EXISTS model_contract_evidence_no_update BEFORE UPDATE ON model_contract_evidence
        BEGIN SELECT RAISE(ABORT, 'model_contract_evidence_immutable'); END;
      CREATE TRIGGER IF NOT EXISTS model_contract_evidence_no_delete BEFORE DELETE ON model_contract_evidence
        BEGIN SELECT RAISE(ABORT, 'model_contract_evidence_immutable'); END;
    `);
  }
}
