import type {
  CanceledInteractionScenario,
  ContinuityFixture,
  ContinuityScenario,
  FormSubmissionScenario,
  InteractionSideEffect,
  StateTransition,
} from "../../../fixtures/continuity/types.js";

export type { ContinuityFixture };

export const CONTINUITY_CAPABILITY = "universal-continuity";

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

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

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;
}

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

function parseStateTransition(value: unknown, index: number): StateTransition {
  if (!isRecord(value)) {
    throw new Error(`stateTrace[${String(index)}] must be an object`);
  }
  if (typeof value.from !== "string" || value.from.trim().length === 0) {
    throw new Error(`stateTrace[${String(index)}].from must be a non-empty string`);
  }
  if (typeof value.event !== "string" || value.event.trim().length === 0) {
    throw new Error(`stateTrace[${String(index)}].event must be a non-empty string`);
  }
  if (typeof value.to !== "string" || value.to.trim().length === 0) {
    throw new Error(`stateTrace[${String(index)}].to must be a non-empty string`);
  }
  return {
    from: value.from,
    event: value.event,
    to: value.to,
  };
}

function parseFormField(value: unknown, index: number): FormSubmissionScenario["fields"][number] {
  if (!isRecord(value)) {
    throw new Error(`fields[${String(index)}] must be an object`);
  }
  if (typeof value.fieldId !== "string" || value.fieldId.trim().length === 0) {
    throw new Error(`fields[${String(index)}].fieldId must be a non-empty string`);
  }
  if (!("enteredValue" in value)) {
    throw new Error(`fields[${String(index)}].enteredValue is required`);
  }
  const allowedSensitivity = new Set(["non-sensitive", "sensitive"]);
  if (typeof value.sensitivity !== "string" || !allowedSensitivity.has(value.sensitivity)) {
    throw new Error(`fields[${String(index)}].sensitivity must be non-sensitive or sensitive`);
  }

  const field: FormSubmissionScenario["fields"][number] = {
    fieldId: value.fieldId,
    enteredValue: value.enteredValue,
    sensitivity: value.sensitivity as FormSubmissionScenario["fields"][number]["sensitivity"],
  };

  if (value.confirmedTransition !== undefined) {
    const allowedTransitions = new Set(["consume", "clear"]);
    if (
      typeof value.confirmedTransition !== "string" ||
      !allowedTransitions.has(value.confirmedTransition)
    ) {
      throw new Error(`fields[${String(index)}].confirmedTransition must be consume or clear`);
    }
    field.confirmedTransition = value.confirmedTransition as "consume" | "clear";
  }

  return field;
}

function parseObservedFieldValue(
  value: unknown,
  index: number,
): FormSubmissionScenario["observedAfterSubmission"][number] {
  if (!isRecord(value)) {
    throw new Error(`observedAfterSubmission[${String(index)}] must be an object`);
  }
  if (typeof value.fieldId !== "string" || value.fieldId.trim().length === 0) {
    throw new Error(`observedAfterSubmission[${String(index)}].fieldId must be a non-empty string`);
  }
  if (!("value" in value)) {
    throw new Error(`observedAfterSubmission[${String(index)}].value is required`);
  }
  return {
    fieldId: value.fieldId,
    value: value.value,
  };
}

function parseFormSubmissionScenario(
  value: Record<string, unknown>,
  index: number,
): FormSubmissionScenario {
  const allowedOutcomes = new Set(["success", "failed", "rejected"]);
  if (
    typeof value.submissionOutcome !== "string" ||
    !allowedOutcomes.has(value.submissionOutcome)
  ) {
    throw new Error(`scenarios[${String(index)}].submissionOutcome must be success, failed, or rejected`);
  }
  if (!Array.isArray(value.fields)) {
    throw new Error(`scenarios[${String(index)}].fields must be an array`);
  }
  if (!Array.isArray(value.observedAfterSubmission)) {
    throw new Error(`scenarios[${String(index)}].observedAfterSubmission must be an array`);
  }

  const scenario: FormSubmissionScenario = {
    scenarioId: typeof value.scenarioId === "string" ? value.scenarioId : "",
    kind: "form-submission",
    submissionOutcome: value.submissionOutcome as FormSubmissionScenario["submissionOutcome"],
    fields: value.fields.map((field, fieldIndex) => parseFormField(field, fieldIndex)),
    observedAfterSubmission: value.observedAfterSubmission.map((observed, observedIndex) =>
      parseObservedFieldValue(observed, observedIndex),
    ),
  };

  if (Array.isArray(value.stateTrace)) {
    scenario.stateTrace = value.stateTrace.map((transition, transitionIndex) =>
      parseStateTransition(transition, transitionIndex),
    );
  }

  return scenario;
}

function parseSideEffect(value: unknown, index: number): InteractionSideEffect {
  if (!isRecord(value)) {
    throw new Error(`sideEffects[${String(index)}] must be an object`);
  }
  if (typeof value.effectId !== "string" || value.effectId.trim().length === 0) {
    throw new Error(`sideEffects[${String(index)}].effectId must be a non-empty string`);
  }
  const allowedKinds = new Set(["navigation", "progress", "mutation", "analytics", "success"]);
  if (typeof value.kind !== "string" || !allowedKinds.has(value.kind)) {
    throw new Error(`sideEffects[${String(index)}].kind is invalid`);
  }
  const allowedScopes = new Set(["parent", "derived", "nested"]);
  if (typeof value.scope !== "string" || !allowedScopes.has(value.scope)) {
    throw new Error(`sideEffects[${String(index)}].scope must be parent, derived, or nested`);
  }
  if (typeof value.tiedToDefaultAction !== "boolean") {
    throw new Error(`sideEffects[${String(index)}].tiedToDefaultAction must be a boolean`);
  }
  if (typeof value.emitted !== "boolean") {
    throw new Error(`sideEffects[${String(index)}].emitted must be a boolean`);
  }

  return {
    effectId: value.effectId,
    kind: value.kind as InteractionSideEffect["kind"],
    scope: value.scope as InteractionSideEffect["scope"],
    tiedToDefaultAction: value.tiedToDefaultAction,
    emitted: value.emitted,
  };
}

function parseCanceledInteractionScenario(
  value: Record<string, unknown>,
  index: number,
): CanceledInteractionScenario {
  if (typeof value.parentState !== "string" || value.parentState.trim().length === 0) {
    throw new Error(`scenarios[${String(index)}].parentState must be a non-empty string`);
  }
  const allowedOutcomes = new Set(["completed", "canceled", "prevented"]);
  if (
    typeof value.interactionOutcome !== "string" ||
    !allowedOutcomes.has(value.interactionOutcome)
  ) {
    throw new Error(
      `scenarios[${String(index)}].interactionOutcome must be completed, canceled, or prevented`,
    );
  }
  if (typeof value.defaultActionId !== "string" || value.defaultActionId.trim().length === 0) {
    throw new Error(`scenarios[${String(index)}].defaultActionId must be a non-empty string`);
  }
  if (!Array.isArray(value.sideEffects)) {
    throw new Error(`scenarios[${String(index)}].sideEffects must be an array`);
  }

  const scenario: CanceledInteractionScenario = {
    scenarioId: typeof value.scenarioId === "string" ? value.scenarioId : "",
    kind: "canceled-interaction",
    parentState: value.parentState,
    interactionOutcome: value.interactionOutcome as CanceledInteractionScenario["interactionOutcome"],
    defaultActionId: value.defaultActionId,
    sideEffects: value.sideEffects.map((effect, effectIndex) => parseSideEffect(effect, effectIndex)),
  };

  if (Array.isArray(value.stateTrace)) {
    scenario.stateTrace = value.stateTrace.map((transition, transitionIndex) =>
      parseStateTransition(transition, transitionIndex),
    );
  }

  return scenario;
}

function parseScenario(value: unknown, index: number): ContinuityScenario {
  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`);
  }

  switch (value.kind) {
    case "form-submission":
      return parseFormSubmissionScenario(value, index);
    case "canceled-interaction":
      return parseCanceledInteractionScenario(value, index);
    default:
      throw new Error(`unsupported scenario kind: ${value.kind}`);
  }
}

export function parseContinuityFixture(input: unknown): ContinuityFixture {
  if (!isRecord(input)) {
    throw new Error("continuity 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 (typeof input.continuityVersion !== "string" || input.continuityVersion.trim().length === 0) {
    throw new Error("continuityVersion 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: ContinuityFixture = {
    fixtureId: input.fixtureId,
    continuityVersion: input.continuityVersion,
    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 observedValueForField(scenario: FormSubmissionScenario, fieldId: string) {
  return scenario.observedAfterSubmission.find((entry) => entry.fieldId === fieldId)?.value;
}

function isPreservationException(field: FormSubmissionScenario["fields"][number]): boolean {
  if (field.sensitivity === "sensitive") {
    return true;
  }
  return field.confirmedTransition === "consume" || field.confirmedTransition === "clear";
}

export function detectUni090Violations(scenario: ContinuityScenario): ContinuityViolation[] {
  if (scenario.kind !== "form-submission") {
    return [];
  }

  if (scenario.submissionOutcome !== "failed" && scenario.submissionOutcome !== "rejected") {
    return [];
  }

  const violations: ContinuityViolation[] = [];

  for (const field of scenario.fields) {
    if (isPreservationException(field)) {
      continue;
    }

    const observedValue = observedValueForField(scenario, field.fieldId);
    if (observedValue === undefined) {
      violations.push({
        scenarioId: scenario.scenarioId,
        kind: "input-not-preserved",
        fact: "non-sensitive field missing after failed or rejected submission",
        details: sanitizeForEvidence({
          fieldId: field.fieldId,
          enteredValue: field.enteredValue,
        }),
      });
      continue;
    }

    if (!valuesEqual(field.enteredValue, observedValue)) {
      violations.push({
        scenarioId: scenario.scenarioId,
        kind: "input-not-preserved",
        fact: "non-sensitive field value changed after failed or rejected submission",
        details: sanitizeForEvidence({
          fieldId: field.fieldId,
          enteredValue: field.enteredValue,
          observedValue,
          submissionOutcome: scenario.submissionOutcome,
        }),
      });
    }
  }

  return violations;
}

export function detectUni091Violations(scenario: ContinuityScenario): ContinuityViolation[] {
  if (scenario.kind !== "canceled-interaction") {
    return [];
  }

  if (scenario.interactionOutcome !== "canceled" && scenario.interactionOutcome !== "prevented") {
    return [];
  }

  const violations: ContinuityViolation[] = [];

  for (const effect of scenario.sideEffects) {
    if (!effect.tiedToDefaultAction || !effect.emitted) {
      continue;
    }

    violations.push({
      scenarioId: scenario.scenarioId,
      kind: "canceled-default-side-effect",
      fact: `${effect.kind} side effect emitted for canceled or prevented default action`,
      details: sanitizeForEvidence({
        parentState: scenario.parentState,
        defaultActionId: scenario.defaultActionId,
        interactionOutcome: scenario.interactionOutcome,
        effect,
      }),
    });
  }

  return violations;
}
