import type {
  CanonicalizationPass,
  DecoderConstraints,
  DecoderFixture,
  DecoderObservation,
  DecoderScenario,
  FallbackCandidate,
  SentinelScenario,
} from "../../../fixtures/decoder/types.js";

export type { DecoderFixture };

export const DECODER_CAPABILITY = "universal-decoder";

export type DecoderViolation = {
  scenarioId: string;
  kind: string;
  fact: string;
  details?: unknown;
};

export function isRuleExcepted(fixture: DecoderFixture, rule: string): boolean {
  return fixture.intentionalException?.rules.includes(rule) ?? false;
}

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null;
}

function parseConstraints(value: unknown): DecoderConstraints {
  if (!isRecord(value)) {
    return {};
  }
  const constraints: DecoderConstraints = {};
  const booleanKeys = [
    "rejectEmptyCoercion",
    "rejectNullCoercion",
    "decimalIntegerOnly",
    "safeIntegerOnly",
    "rejectNonFinite",
    "rejectMalformedDate",
    "idempotent",
    "reversible",
    "collisionSafe",
  ] as const satisfies ReadonlyArray<keyof DecoderConstraints>;
  for (const key of booleanKeys) {
    if (value[key] === true) {
      constraints[key] = true;
    }
  }
  return constraints;
}

function parseObservation(value: unknown): DecoderObservation {
  if (!isRecord(value)) {
    throw new Error("observation must be an object");
  }
  if (typeof value.accepted !== "boolean") {
    throw new Error("observation.accepted must be a boolean");
  }
  const observation: DecoderObservation = { accepted: value.accepted };
  if (value.value !== undefined) {
    observation.value = value.value;
  }
  if (value.error !== undefined) {
    if (!isRecord(value.error)) {
      throw new Error("observation.error must be an object");
    }
    if (typeof value.error.kind !== "string" || typeof value.error.message !== "string") {
      throw new Error("observation.error must include kind and message");
    }
    observation.error = {
      kind: value.error.kind,
      message: value.error.message,
      ...(typeof value.error.rawLeak === "string" ? { rawLeak: value.error.rawLeak } : {}),
    };
  }
  return observation;
}

function parseFallbackCandidates(value: unknown): FallbackCandidate[] {
  if (!Array.isArray(value)) {
    throw new Error("candidates must be an array");
  }
  return value.map((candidate, index) => {
    if (!isRecord(candidate) || typeof candidate.valid !== "boolean") {
      throw new Error(`candidates[${String(index)}] must include valid boolean`);
    }
    return { value: candidate.value, valid: candidate.valid };
  });
}

function parseCanonicalizationPass(value: unknown): CanonicalizationPass {
  if (!isRecord(value)) {
    throw new Error("passes must be an object");
  }
  if (!("input" in value) || !("firstPass" in value) || !("secondPass" in value)) {
    throw new Error("passes must include input, firstPass, and secondPass");
  }
  const pass: CanonicalizationPass = {
    input: value.input,
    firstPass: value.firstPass,
    secondPass: value.secondPass,
  };
  if ("decodeAfterCanonicalize" in value) {
    pass.decodeAfterCanonicalize = value.decodeAfterCanonicalize;
  }
  return pass;
}

function parseSentinel(value: unknown): SentinelScenario {
  if (!isRecord(value)) {
    throw new Error("sentinel must be an object");
  }
  if (typeof value.state !== "string" || !Array.isArray(value.validValues)) {
    throw new Error("sentinel must include state and validValues");
  }
  const allowedStates = new Set([
    "missing",
    "pending",
    "unknown",
    "empty",
    "zero",
    "failed",
  ]);
  if (!allowedStates.has(value.state)) {
    throw new Error(`unsupported sentinel state: ${value.state}`);
  }
  const sentinel: SentinelScenario = {
    state: value.state as SentinelScenario["state"],
    decodedAs: value.decodedAs,
    validValues: value.validValues,
  };
  if (Array.isArray(value.declaredEquivalences)) {
    sentinel.declaredEquivalences = value.declaredEquivalences.map((entry, index) => {
      if (!isRecord(entry) || !Array.isArray(entry.states)) {
        throw new Error(`declaredEquivalences[${String(index)}] must include states`);
      }
      return {
        states: entry.states as SentinelScenario["state"][],
        equivalentValue: entry.equivalentValue,
      };
    });
  }
  return sentinel;
}

function parseScenario(value: unknown, index: number): DecoderScenario {
  if (!isRecord(value)) {
    throw new Error(`scenarios[${String(index)}] must be an object`);
  }
  if (typeof value.scenarioId !== "string" || value.scenarioId.trim().length === 0) {
    throw new Error(`scenarios[${String(index)}].scenarioId must be a non-empty string`);
  }
  if (typeof value.kind !== "string") {
    throw new Error(`scenarios[${String(index)}].kind must be a string`);
  }

  const constraints = parseConstraints(value.constraints);
  const scenarioId = value.scenarioId;

  switch (value.kind) {
    case "integer":
      return {
        scenarioId,
        kind: "integer",
        input: value.input,
        constraints,
        observation: parseObservation(value.observation),
      };
    case "date":
      if (typeof value.input !== "string") {
        throw new Error(`scenarios[${String(index)}].input must be a string for date kind`);
      }
      if (typeof value.malformed !== "boolean") {
        throw new Error(`scenarios[${String(index)}].malformed must be a boolean for date kind`);
      }
      return {
        scenarioId,
        kind: "date",
        input: value.input,
        malformed: value.malformed,
        constraints,
        observation: parseObservation(value.observation),
      };
    case "canonicalization":
      return {
        scenarioId,
        kind: "canonicalization",
        passes: parseCanonicalizationPass(value.passes),
        constraints,
        ...(isRecord(value.collisionPair)
          ? {
              collisionPair: {
                left: value.collisionPair.left,
                right: value.collisionPair.right,
                canonical: value.collisionPair.canonical,
              },
            }
          : {}),
      };
    case "fallback":
      return {
        scenarioId,
        kind: "fallback",
        candidates: parseFallbackCandidates(value.candidates),
        selectedIndex: typeof value.selectedIndex === "number" ? value.selectedIndex : -1,
        constraints,
        observation: parseObservation(value.observation),
      };
    case "sentinel":
      return {
        scenarioId,
        kind: "sentinel",
        sentinel: parseSentinel(value.sentinel),
        constraints,
      };
    case "error-boundary":
      if (typeof value.driverException !== "string") {
        throw new Error(`scenarios[${String(index)}].driverException must be a string`);
      }
      return {
        scenarioId,
        kind: "error-boundary",
        input: value.input,
        constraints,
        driverException: value.driverException,
        observation: parseObservation(value.observation),
      };
    default:
      throw new Error(`unsupported scenario kind: ${value.kind}`);
  }
}

export function parseDecoderFixture(input: unknown): DecoderFixture {
  if (!isRecord(input)) {
    throw new Error("decoder fixture must be an object");
  }
  if (typeof input.fixtureId !== "string" || input.fixtureId.trim().length === 0) {
    throw new Error("fixtureId must be a non-empty string");
  }
  if (!Array.isArray(input.scenarios) || input.scenarios.length === 0) {
    throw new Error("scenarios must be a non-empty array");
  }

  const fixture: DecoderFixture = {
    fixtureId: input.fixtureId,
    scenarios: input.scenarios.map((scenario, index) => parseScenario(scenario, index)),
  };

  if (isRecord(input.intentionalException)) {
    const rules = input.intentionalException.rules;
    const reason = input.intentionalException.reason;
    if (!Array.isArray(rules) || rules.some((rule) => typeof rule !== "string")) {
      throw new Error("intentionalException.rules must be a string array");
    }
    const parsedRules = rules.filter((rule): rule is string => typeof rule === "string");
    if (parsedRules.length !== rules.length) {
      throw new Error("intentionalException.rules must be a string array");
    }
    if (typeof reason !== "string" || reason.trim().length === 0) {
      throw new Error("intentionalException.reason must be a non-empty string");
    }
    fixture.intentionalException = { rules: parsedRules, reason };
  }

  return fixture;
}

function hasUnsupportedNumericSyntax(input: unknown): boolean {
  if (typeof input !== "string") {
    return false;
  }
  const trimmed = input.trim();
  return /^[+-]?0x[0-9a-f]+$/i.test(trimmed) || /^[+-]?\d*\.?\d+e[+-]?\d+$/i.test(trimmed);
}

function isUnsafeInteger(value: unknown): boolean {
  if (typeof value !== "number") {
    return false;
  }
  if (!Number.isInteger(value)) {
    return false;
  }
  return value > Number.MAX_SAFE_INTEGER || value < Number.MIN_SAFE_INTEGER;
}

function isNonFiniteNumber(value: unknown): boolean {
  return typeof value === "number" && !Number.isFinite(value);
}

function valuesEqual(left: unknown, right: unknown): boolean {
  return JSON.stringify(left) === JSON.stringify(right);
}

export function sanitizeForEvidence(value: unknown): unknown {
  if (typeof value === "number" && !Number.isFinite(value)) {
    return String(value);
  }
  if (Array.isArray(value)) {
    return value.map((entry) => sanitizeForEvidence(entry));
  }
  if (isRecord(value)) {
    return Object.fromEntries(
      Object.entries(value).map(([key, entry]) => [key, sanitizeForEvidence(entry)]),
    );
  }
  return value;
}

function sentinelAllowedEquivalent(
  sentinel: SentinelScenario,
  decodedAs: unknown,
): boolean {
  return (sentinel.declaredEquivalences ?? []).some((equivalence) => {
    return (
      equivalence.states.includes(sentinel.state) &&
      valuesEqual(equivalence.equivalentValue, decodedAs)
    );
  });
}

export function detectUni030Violations(scenario: DecoderScenario): DecoderViolation[] {
  if (scenario.kind === "integer") {
    const violations: DecoderViolation[] = [];
    const { input, constraints, observation } = scenario;

    if (
      constraints.rejectEmptyCoercion &&
      input === "" &&
      observation.accepted &&
      typeof observation.value === "number"
    ) {
      violations.push({
        scenarioId: scenario.scenarioId,
        kind: "lossy-empty-coercion",
        fact: "empty string coerced to numeric value",
        details: sanitizeForEvidence({ input, value: observation.value }),
      });
    }

    if (
      constraints.rejectNullCoercion &&
      input === null &&
      observation.accepted &&
      typeof observation.value === "number"
    ) {
      violations.push({
        scenarioId: scenario.scenarioId,
        kind: "lossy-null-coercion",
        fact: "null coerced to numeric value",
        details: sanitizeForEvidence({ input, value: observation.value }),
      });
    }

    if (
      constraints.decimalIntegerOnly &&
      hasUnsupportedNumericSyntax(input) &&
      observation.accepted
    ) {
      violations.push({
        scenarioId: scenario.scenarioId,
        kind: "unsupported-numeric-syntax",
        fact: "hex or scientific notation accepted where decimal integer required",
        details: sanitizeForEvidence({ input, value: observation.value }),
      });
    }

    if (
      constraints.safeIntegerOnly &&
      observation.accepted &&
      isUnsafeInteger(observation.value)
    ) {
      violations.push({
        scenarioId: scenario.scenarioId,
        kind: "unsafe-integer",
        fact: "integer outside safe range accepted",
        details: sanitizeForEvidence({ input, value: observation.value }),
      });
    }

    if (
      constraints.rejectNonFinite &&
      observation.accepted &&
      isNonFiniteNumber(observation.value)
    ) {
      violations.push({
        scenarioId: scenario.scenarioId,
        kind: "non-finite-number",
        fact: "non-finite number accepted",
        details: sanitizeForEvidence({ input, value: observation.value }),
      });
    }

    return violations;
  }

  if (scenario.kind === "date") {
    if (
      scenario.constraints.rejectMalformedDate &&
      scenario.malformed &&
      scenario.observation.accepted
    ) {
      return [
        {
          scenarioId: scenario.scenarioId,
          kind: "malformed-date-normalization",
          fact: "malformed date input normalized to accepted value",
          details: {
            input: scenario.input,
            value: scenario.observation.value,
          },
        },
      ];
    }
  }

  return [];
}

export function detectUni031Violations(scenario: DecoderScenario): DecoderViolation[] {
  if (scenario.kind !== "canonicalization") {
    return [];
  }

  const violations: DecoderViolation[] = [];
  const { passes, constraints } = scenario;

  if (constraints.idempotent && !valuesEqual(passes.firstPass, passes.secondPass)) {
    violations.push({
      scenarioId: scenario.scenarioId,
      kind: "non-idempotent-canonicalization",
      fact: "canonicalization is not idempotent",
      details: {
        firstPass: passes.firstPass,
        secondPass: passes.secondPass,
      },
    });
  }

  if (
    constraints.collisionSafe &&
    scenario.collisionPair !== undefined &&
    !valuesEqual(scenario.collisionPair.left, scenario.collisionPair.right) &&
    valuesEqual(scenario.collisionPair.left, scenario.collisionPair.canonical) &&
    valuesEqual(scenario.collisionPair.right, scenario.collisionPair.canonical)
  ) {
    violations.push({
      scenarioId: scenario.scenarioId,
      kind: "canonicalization-collision",
      fact: "distinct inputs collapse to the same canonical value",
      details: scenario.collisionPair,
    });
  }

  if (
    constraints.reversible &&
    passes.decodeAfterCanonicalize !== undefined &&
    !valuesEqual(passes.decodeAfterCanonicalize, passes.input)
  ) {
    violations.push({
      scenarioId: scenario.scenarioId,
      kind: "non-round-trip-canonicalization",
      fact: "canonicalization breaks promised round-trip",
      details: {
        input: passes.input,
        decodeAfterCanonicalize: passes.decodeAfterCanonicalize,
      },
    });
  }

  return violations;
}

export function detectUni032Violations(scenario: DecoderScenario): DecoderViolation[] {
  if (scenario.kind !== "fallback") {
    return [];
  }

  const selected = scenario.candidates[scenario.selectedIndex];
  if (selected === undefined) {
    return [
      {
        scenarioId: scenario.scenarioId,
        kind: "invalid-fallback-selection",
        fact: "fallback selected index is out of range",
        details: { selectedIndex: scenario.selectedIndex },
      },
    ];
  }

  const validSecondaryIndex = scenario.candidates.findIndex(
    (candidate, index) => candidate.valid && index !== scenario.selectedIndex,
  );
  const validSecondary =
    validSecondaryIndex >= 0 ? scenario.candidates[validSecondaryIndex] : undefined;

  if (
    !selected.valid &&
    selected.value !== null &&
    selected.value !== undefined &&
    validSecondary !== undefined &&
    scenario.observation.accepted &&
    valuesEqual(scenario.observation.value, selected.value)
  ) {
    return [
      {
        scenarioId: scenario.scenarioId,
        kind: "fallback-shadow",
        fact: "invalid non-null primary shadows valid secondary candidate",
        details: {
          selectedIndex: scenario.selectedIndex,
          selectedValue: selected.value,
          validSecondaryIndex,
          validSecondaryValue: validSecondary.value,
        },
      },
    ];
  }

  return [];
}

export function detectUni033Violations(scenario: DecoderScenario): DecoderViolation[] {
  if (scenario.kind !== "sentinel") {
    return [];
  }

  const { sentinel } = scenario;
  const decodedMatchesValid = sentinel.validValues.some((value) =>
    valuesEqual(value, sentinel.decodedAs),
  );
  if (decodedMatchesValid) {
    return [
      {
        scenarioId: scenario.scenarioId,
        kind: "sentinel-collapse",
        fact: `sentinel state ${sentinel.state} collapsed into valid value`,
        details: {
          state: sentinel.state,
          decodedAs: sentinel.decodedAs,
        },
      },
    ];
  }

  if (sentinelAllowedEquivalent(sentinel, sentinel.decodedAs)) {
    return [];
  }

  if (sentinel.state === "zero" && sentinel.decodedAs === 0) {
    return [];
  }

  if (sentinel.state === "empty" && sentinel.decodedAs === "") {
    return [];
  }

  const collapsibleStates = new Set<SentinelScenario["state"]>([
    "missing",
    "pending",
    "unknown",
    "failed",
  ]);
  if (
    collapsibleStates.has(sentinel.state) &&
    (sentinel.decodedAs === 0 || sentinel.decodedAs === "")
  ) {
    return [
      {
        scenarioId: scenario.scenarioId,
        kind: "sentinel-collapse",
        fact: `sentinel state ${sentinel.state} collapsed into ambiguous empty/zero value`,
        details: {
          state: sentinel.state,
          decodedAs: sentinel.decodedAs,
        },
      },
    ];
  }

  return [];
}

export function detectUni034Violations(scenario: DecoderScenario): DecoderViolation[] {
  if (scenario.kind !== "error-boundary") {
    return [];
  }

  const { observation, driverException } = scenario;
  if (!observation.accepted && observation.error !== undefined) {
    const error = observation.error;
    const leaked =
      error.rawLeak !== undefined ||
      error.message.includes(driverException) ||
      error.kind === "Error" ||
      error.kind.endsWith("Error") ||
      /PgError|SqliteError|MongoError|DriverError/i.test(error.message);

    if (leaked) {
      return [
        {
          scenarioId: scenario.scenarioId,
          kind: "raw-exception-leak",
          fact: "raw driver/runtime exception leaked at public decoder boundary",
          details: {
            driverException,
            publicError: error,
          },
        },
      ];
    }
  }

  return [];
}
