import type { ObjectSchema, SchemaDescriptor } from "./index.ts";

export type ContractKind =
  | "api"
  | "action"
  | "webhook"
  | "realtime"
  | "queue"
  | "cron"
  | "pubsub"
  | "plugin"
  | "config"
  | "env";

export interface ContractDefinition<T extends object = Record<string, unknown>> {
  kind: ContractKind;
  name: string;
  version: number;
  payload: ObjectSchema<T> | SchemaDescriptor;
  consumers?: string[];
  description?: string;
}

export interface ContractRecord {
  kind: ContractKind;
  name: string;
  version: number;
  payload: SchemaDescriptor;
  consumers: string[];
  description?: string;
}

export interface ContractSnapshot {
  format: 1;
  contracts: ContractRecord[];
}

export interface ContractIssue {
  code:
    | "WRN-CONTRACT-REMOVED"
    | "WRN-CONTRACT-FIELD-REMOVED"
    | "WRN-CONTRACT-FIELD-REQUIRED"
    | "WRN-CONTRACT-FIELD-TYPE"
    | "WRN-CONTRACT-RULE-TIGHTENED";
  contract: string;
  field?: string;
  message: string;
  consumers: string[];
}

function isDescriptor(value: ObjectSchema<object> | SchemaDescriptor): value is SchemaDescriptor {
  return "type" in value && value.type === "object" && "fields" in value;
}

function keyOf(contract: Pick<ContractRecord, "kind" | "name" | "version">): string {
  return `${contract.kind}:${contract.name}@${contract.version}`;
}

export function defineContract<T extends object>(
  definition: ContractDefinition<T>,
): ContractDefinition<T> {
  if (!definition.name.trim()) throw new TypeError("Contract name is required.");
  if (!Number.isInteger(definition.version) || definition.version < 1) {
    throw new TypeError("Contract version must be a positive integer.");
  }
  return definition;
}

export function defineEvent<T extends object>(
  definition: Omit<ContractDefinition<T>, "kind"> & { kind?: "realtime" | "pubsub" },
): ContractDefinition<T> {
  return defineContract({ ...definition, kind: definition.kind ?? "pubsub" });
}

export class ContractRegistry {
  private readonly records = new Map<string, ContractRecord>();

  register<T extends object>(definition: ContractDefinition<T>): this {
    const valid = defineContract(definition);
    const record: ContractRecord = {
      kind: valid.kind,
      name: valid.name,
      version: valid.version,
      payload: isDescriptor(valid.payload)
        ? structuredClone(valid.payload)
        : valid.payload.describe(),
      consumers: [...new Set(valid.consumers ?? [])].sort(),
      description: valid.description,
    };
    const key = keyOf(record);
    if (this.records.has(key)) throw new Error(`Duplicate contract: ${key}`);
    this.records.set(key, record);
    return this;
  }

  snapshot(): ContractSnapshot {
    return {
      format: 1,
      contracts: [...this.records.values()]
        .map((record) => structuredClone(record))
        .sort((left, right) => keyOf(left).localeCompare(keyOf(right))),
    };
  }
}

function ruleStrength(
  rule: SchemaDescriptor["fields"][string]["rules"][number],
): number | undefined {
  if (rule.kind === "min" || rule.kind === "length") return rule.n;
  if (rule.kind === "max") return -rule.n;
  return undefined;
}

export function checkContractCompatibility(
  previous: ContractSnapshot,
  current: ContractSnapshot,
): ContractIssue[] {
  const now = new Map(current.contracts.map((contract) => [keyOf(contract), contract]));
  const issues: ContractIssue[] = [];
  for (const before of previous.contracts) {
    const contract = keyOf(before);
    const after = now.get(contract);
    if (!after) {
      issues.push({
        code: "WRN-CONTRACT-REMOVED",
        contract,
        message: `Contract removed: ${contract}`,
        consumers: before.consumers,
      });
      continue;
    }
    for (const [field, oldField] of Object.entries(before.payload.fields)) {
      const newField = after.payload.fields[field];
      if (!newField) {
        issues.push({
          code: "WRN-CONTRACT-FIELD-REMOVED",
          contract,
          field,
          message: `Field removed: ${field}`,
          consumers: before.consumers,
        });
        continue;
      }
      if (oldField.type !== newField.type) {
        issues.push({
          code: "WRN-CONTRACT-FIELD-TYPE",
          contract,
          field,
          message: `Field type changed: ${field} (${oldField.type} -> ${newField.type})`,
          consumers: before.consumers,
        });
      }
      if (oldField.optional && !newField.optional) {
        issues.push({
          code: "WRN-CONTRACT-FIELD-REQUIRED",
          contract,
          field,
          message: `Optional field became required: ${field}`,
          consumers: before.consumers,
        });
      }
      for (const newRule of newField.rules) {
        const oldRule = oldField.rules.find((rule) => rule.kind === newRule.kind);
        const newStrength = ruleStrength(newRule);
        const oldStrength = oldRule && ruleStrength(oldRule);
        const enumNarrowed =
          newRule.kind === "oneOf" &&
          oldRule?.kind === "oneOf" &&
          oldRule.values.some((value) => !newRule.values.includes(value));
        const constraintAdded =
          !oldRule &&
          [
            "email",
            "url",
            "uuid",
            "date",
            "pattern",
            "integer",
            "min",
            "max",
            "length",
            "oneOf",
          ].includes(newRule.kind);
        if (
          constraintAdded ||
          enumNarrowed ||
          (newStrength !== undefined && oldStrength !== undefined && newStrength > oldStrength)
        ) {
          issues.push({
            code: "WRN-CONTRACT-RULE-TIGHTENED",
            contract,
            field,
            message: `Validation tightened: ${field}.${newRule.kind}`,
            consumers: before.consumers,
          });
        }
      }
    }
    for (const [field, descriptor] of Object.entries(after.payload.fields)) {
      if (!before.payload.fields[field] && !descriptor.optional) {
        issues.push({
          code: "WRN-CONTRACT-FIELD-REQUIRED",
          contract,
          field,
          message: `Required field added: ${field}`,
          consumers: before.consumers,
        });
      }
    }
  }
  return issues;
}
