import type {
  ExecutionOrdering,
  IsolationFixture,
  IsolationScenario,
  MetamorphicExecutionScenario,
  ModeIsolationScenario,
  ParallelExecution,
  RepetitionRun,
  ResourceLeak,
  ScenarioExecutionResult,
} from "../../../fixtures/isolation/types.js";

export type { IsolationFixture };

export const ISOLATION_CAPABILITY = "universal-isolation";

export type IsolationViolation = {
  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: IsolationFixture, rule: string): boolean {
  return fixture.intentionalException?.rules.includes(rule) ?? false;
}

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

function parseOrdering(value: unknown, index: number): ExecutionOrdering {
  if (!isRecord(value)) {
    throw new Error(`orderings[${String(index)}] must be an object`);
  }
  if (typeof value.orderingId !== "string" || value.orderingId.trim().length === 0) {
    throw new Error(`orderings[${String(index)}].orderingId must be a non-empty string`);
  }
  if (!Array.isArray(value.sequence) || value.sequence.some((entry) => typeof entry !== "string")) {
    throw new Error(`orderings[${String(index)}].sequence must be a string array`);
  }
  if (!Array.isArray(value.observedResults)) {
    throw new Error(`orderings[${String(index)}].observedResults must be an array`);
  }
  return {
    orderingId: value.orderingId,
    sequence: value.sequence.filter((entry): entry is string => typeof entry === "string"),
    observedResults: value.observedResults.map((entry, resultIndex) =>
      parseScenarioExecutionResult(entry, resultIndex),
    ),
  };
}

function parseRepetition(value: unknown, index: number): RepetitionRun {
  if (!isRecord(value)) {
    throw new Error(`repetitions[${String(index)}] must be an object`);
  }
  if (typeof value.repetitionId !== "string" || value.repetitionId.trim().length === 0) {
    throw new Error(`repetitions[${String(index)}].repetitionId must be a non-empty string`);
  }
  if (typeof value.runCount !== "number") {
    throw new Error(`repetitions[${String(index)}].runCount must be a number`);
  }
  if (!Array.isArray(value.observedResults)) {
    throw new Error(`repetitions[${String(index)}].observedResults must be an array`);
  }
  return {
    repetitionId: value.repetitionId,
    runCount: value.runCount,
    observedResults: value.observedResults.map((entry, resultIndex) =>
      parseScenarioExecutionResult(entry, resultIndex),
    ),
  };
}

function parseParallelRun(value: unknown, index: number): ParallelExecution {
  if (!isRecord(value)) {
    throw new Error(`parallelRuns[${String(index)}] must be an object`);
  }
  if (typeof value.parallelId !== "string" || value.parallelId.trim().length === 0) {
    throw new Error(`parallelRuns[${String(index)}].parallelId must be a non-empty string`);
  }
  if (!Array.isArray(value.contexts)) {
    throw new Error(`parallelRuns[${String(index)}].contexts must be an array`);
  }
  return {
    parallelId: value.parallelId,
    contexts: value.contexts.map((context, contextIndex) => {
      if (!isRecord(context)) {
        throw new Error(`parallelRuns[${String(index)}].contexts[${String(contextIndex)}] must be an object`);
      }
      if (typeof context.contextId !== "string") {
        throw new Error(
          `parallelRuns[${String(index)}].contexts[${String(contextIndex)}].contextId must be a string`,
        );
      }
      if (typeof context.scenarioId !== "string") {
        throw new Error(
          `parallelRuns[${String(index)}].contexts[${String(contextIndex)}].scenarioId must be a string`,
        );
      }
      if (typeof context.identity !== "string") {
        throw new Error(
          `parallelRuns[${String(index)}].contexts[${String(contextIndex)}].identity must be a string`,
        );
      }
      if (!("result" in context)) {
        throw new Error(
          `parallelRuns[${String(index)}].contexts[${String(contextIndex)}].result is required`,
        );
      }
      return {
        contextId: context.contextId,
        scenarioId: context.scenarioId,
        identity: context.identity,
        result: context.result,
      };
    }),
  };
}

function parseResourceLeak(value: unknown, index: number): ResourceLeak {
  if (!isRecord(value)) {
    throw new Error(`leakedResources[${String(index)}] must be an object`);
  }
  const allowedKinds = new Set([
    "shared-state",
    "real-home-directory",
    "stale-process",
    "cache",
    "port",
    "database-row",
    "clock",
    "random-source",
    "artifact",
  ]);
  if (typeof value.kind !== "string" || !allowedKinds.has(value.kind)) {
    throw new Error(`leakedResources[${String(index)}].kind is invalid`);
  }
  if (typeof value.leakedFromContext !== "string") {
    throw new Error(`leakedResources[${String(index)}].leakedFromContext must be a string`);
  }
  if (typeof value.observedInContext !== "string") {
    throw new Error(`leakedResources[${String(index)}].observedInContext must be a string`);
  }
  if (typeof value.detail !== "string") {
    throw new Error(`leakedResources[${String(index)}].detail must be a string`);
  }
  return {
    kind: value.kind as ResourceLeak["kind"],
    leakedFromContext: value.leakedFromContext,
    observedInContext: value.observedInContext,
    detail: value.detail,
  };
}

function parseMetamorphicScenario(value: Record<string, unknown>, index: number): MetamorphicExecutionScenario {
  if (!isRecord(value.baseline)) {
    throw new Error(`scenarios[${String(index)}].baseline must be an object`);
  }
  const baseline = parseScenarioExecutionResult(value.baseline, 0);
  if (!isRecord(value.constraints)) {
    throw new Error(`scenarios[${String(index)}].constraints must be an object`);
  }
  const constraints = value.constraints;
  if (!Array.isArray(value.orderings)) {
    throw new Error(`scenarios[${String(index)}].orderings must be an array`);
  }
  if (!Array.isArray(value.repetitions)) {
    throw new Error(`scenarios[${String(index)}].repetitions must be an array`);
  }

  const scenario: MetamorphicExecutionScenario = {
    scenarioId: typeof value.scenarioId === "string" ? value.scenarioId : "",
    kind: "metamorphic-execution",
    baseline,
    constraints: {
      orderInvariant: constraints.orderInvariant === true,
      repetitionInvariant: constraints.repetitionInvariant === true,
      parallelIndependent: constraints.parallelIndependent === true,
      stableIdentity: constraints.stableIdentity === true,
    },
    orderings: value.orderings.map((ordering, orderingIndex) => parseOrdering(ordering, orderingIndex)),
    repetitions: value.repetitions.map((repetition, repetitionIndex) =>
      parseRepetition(repetition, repetitionIndex),
    ),
  };

  if (Array.isArray(value.parallelRuns)) {
    scenario.parallelRuns = value.parallelRuns.map((parallelRun, parallelIndex) =>
      parseParallelRun(parallelRun, parallelIndex),
    );
  }
  if (Array.isArray(value.leakedResources)) {
    scenario.leakedResources = value.leakedResources.map((leak, leakIndex) =>
      parseResourceLeak(leak, leakIndex),
    );
  }

  return scenario;
}

function parseModeIsolationScenario(value: Record<string, unknown>, index: number): ModeIsolationScenario {
  const allowedModes = new Set(["demo", "preview", "fixture", "offline"]);
  if (typeof value.mode !== "string" || !allowedModes.has(value.mode)) {
    throw new Error(`scenarios[${String(index)}].mode must be demo, preview, fixture, or offline`);
  }
  if (!isRecord(value.isolationContract)) {
    throw new Error(`scenarios[${String(index)}].isolationContract must be an object`);
  }
  const contract = value.isolationContract;
  if (typeof contract.declaresIsolation !== "boolean") {
    throw new Error(`scenarios[${String(index)}].isolationContract.declaresIsolation must be a boolean`);
  }
  if (
    !Array.isArray(contract.allowedDependencyTargets) ||
    contract.allowedDependencyTargets.some((entry) => typeof entry !== "string")
  ) {
    throw new Error(
      `scenarios[${String(index)}].isolationContract.allowedDependencyTargets must be a string array`,
    );
  }
  if (!Array.isArray(value.accessAttempts)) {
    throw new Error(`scenarios[${String(index)}].accessAttempts must be an array`);
  }

  return {
    scenarioId: typeof value.scenarioId === "string" ? value.scenarioId : "",
    kind: "mode-isolation",
    mode: value.mode as ModeIsolationScenario["mode"],
    isolationContract: {
      declaresIsolation: contract.declaresIsolation,
      allowedDependencyTargets: contract.allowedDependencyTargets.filter(
        (entry): entry is string => typeof entry === "string",
      ),
    },
    accessAttempts: value.accessAttempts.map((attempt, attemptIndex) => {
      if (!isRecord(attempt)) {
        throw new Error(`scenarios[${String(index)}].accessAttempts[${String(attemptIndex)}] must be an object`);
      }
      if (typeof attempt.target !== "string") {
        throw new Error(
          `scenarios[${String(index)}].accessAttempts[${String(attemptIndex)}].target must be a string`,
        );
      }
      const allowedClassifications = new Set(["live", "mock", "local-fixture", "production"]);
      if (
        typeof attempt.classification !== "string" ||
        !allowedClassifications.has(attempt.classification)
      ) {
        throw new Error(
          `scenarios[${String(index)}].accessAttempts[${String(attemptIndex)}].classification is invalid`,
        );
      }
      if (typeof attempt.reached !== "boolean") {
        throw new Error(
          `scenarios[${String(index)}].accessAttempts[${String(attemptIndex)}].reached must be a boolean`,
        );
      }
      return {
        target: attempt.target,
        classification: attempt.classification as ModeIsolationScenario["accessAttempts"][number]["classification"],
        reached: attempt.reached,
        ...(typeof attempt.channel === "string" ? { channel: attempt.channel } : {}),
      };
    }),
  };
}

function parseScenario(value: unknown, index: number): IsolationScenario {
  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 "metamorphic-execution":
      return parseMetamorphicScenario(value, index);
    case "mode-isolation":
      return parseModeIsolationScenario(value, index);
    default:
      throw new Error(`unsupported scenario kind: ${value.kind}`);
  }
}

export function parseIsolationFixture(input: unknown): IsolationFixture {
  if (!isRecord(input)) {
    throw new Error("isolation 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.isolationVersion !== "string" || input.isolationVersion.trim().length === 0) {
    throw new Error("isolationVersion 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: IsolationFixture = {
    fixtureId: input.fixtureId,
    isolationVersion: input.isolationVersion,
    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 collectIdentityObservations(
  scenario: MetamorphicExecutionScenario,
): Array<{ scenarioId: string; identity: string; source: string }> {
  const observations: Array<{ scenarioId: string; identity: string; source: string }> = [];

  for (const ordering of scenario.orderings) {
    for (const result of ordering.observedResults) {
      observations.push({
        scenarioId: result.scenarioId,
        identity: result.identity,
        source: `ordering:${ordering.orderingId}`,
      });
    }
  }

  for (const repetition of scenario.repetitions) {
    for (const [index, result] of repetition.observedResults.entries()) {
      observations.push({
        scenarioId: result.scenarioId,
        identity: result.identity,
        source: `repetition:${repetition.repetitionId}:${String(index)}`,
      });
    }
  }

  for (const parallelRun of scenario.parallelRuns ?? []) {
    for (const context of parallelRun.contexts) {
      observations.push({
        scenarioId: context.scenarioId,
        identity: context.identity,
        source: `parallel:${parallelRun.parallelId}:${context.contextId}`,
      });
    }
  }

  observations.push({
    scenarioId: scenario.baseline.scenarioId,
    identity: scenario.baseline.identity,
    source: "baseline",
  });

  return observations;
}

export function detectUni070Violations(scenario: IsolationScenario): IsolationViolation[] {
  if (scenario.kind !== "metamorphic-execution") {
    return [];
  }

  const violations: IsolationViolation[] = [];
  const { baseline, constraints } = scenario;

  if (constraints.stableIdentity) {
    const identitiesByScenario = new Map<string, Set<string>>();
    for (const observation of collectIdentityObservations(scenario)) {
      const identities = identitiesByScenario.get(observation.scenarioId) ?? new Set<string>();
      identities.add(observation.identity);
      identitiesByScenario.set(observation.scenarioId, identities);
    }

    for (const [scenarioId, identities] of identitiesByScenario) {
      if (identities.size > 1) {
        violations.push({
          scenarioId: scenario.scenarioId,
          kind: "identity-drift",
          fact: "scenario identity changed across execution contexts",
          details: {
            scenarioId,
            identities: [...identities],
          },
        });
      }
    }
  }

  if (constraints.orderInvariant) {
    const referenceByScenario = new Map<string, ScenarioExecutionResult>();
    referenceByScenario.set(baseline.scenarioId, baseline);

    for (const ordering of scenario.orderings) {
      for (const observed of ordering.observedResults) {
        const reference = referenceByScenario.get(observed.scenarioId);
        if (reference === undefined) {
          referenceByScenario.set(observed.scenarioId, observed);
          continue;
        }
        if (!valuesEqual(reference.result, observed.result)) {
          violations.push({
            scenarioId: scenario.scenarioId,
            kind: "order-dependent-result",
            fact: "scenario result changed across execution orderings",
            details: {
              orderingId: ordering.orderingId,
              scenarioId: observed.scenarioId,
              referenceResult: sanitizeForEvidence(reference.result),
              observedResult: sanitizeForEvidence(observed.result),
            },
          });
        }
      }
    }
  }

  if (constraints.repetitionInvariant) {
    for (const repetition of scenario.repetitions) {
      for (const observed of repetition.observedResults) {
        if (observed.scenarioId !== baseline.scenarioId) {
          continue;
        }
        if (!valuesEqual(baseline.result, observed.result)) {
          violations.push({
            scenarioId: scenario.scenarioId,
            kind: "repetition-drift",
            fact: "scenario result changed across repeated execution",
            details: {
              repetitionId: repetition.repetitionId,
              baselineResult: sanitizeForEvidence(baseline.result),
              observedResult: sanitizeForEvidence(observed.result),
            },
          });
        }
        if (observed.identity !== baseline.identity) {
          violations.push({
            scenarioId: scenario.scenarioId,
            kind: "repetition-identity-drift",
            fact: "scenario identity changed across repeated execution",
            details: {
              repetitionId: repetition.repetitionId,
              baselineIdentity: baseline.identity,
              observedIdentity: observed.identity,
            },
          });
        }
      }
    }
  }

  if (constraints.parallelIndependent) {
    for (const parallelRun of scenario.parallelRuns ?? []) {
      const resultsByScenario = new Map<string, Array<{ contextId: string; result: unknown }>>();
      for (const context of parallelRun.contexts) {
        const entries = resultsByScenario.get(context.scenarioId) ?? [];
        entries.push({ contextId: context.contextId, result: context.result });
        resultsByScenario.set(context.scenarioId, entries);
      }

      for (const [scenarioId, entries] of resultsByScenario) {
        const reference = entries[0];
        if (reference === undefined) {
          continue;
        }
        for (const entry of entries.slice(1)) {
          if (!valuesEqual(reference.result, entry.result)) {
            violations.push({
              scenarioId: scenario.scenarioId,
              kind: "parallel-cross-contamination",
              fact: "parallel execution contexts produced divergent results for the same scenario",
              details: {
                parallelId: parallelRun.parallelId,
                scenarioId,
                referenceContextId: reference.contextId,
                divergentContextId: entry.contextId,
                referenceResult: sanitizeForEvidence(reference.result),
                observedResult: sanitizeForEvidence(entry.result),
              },
            });
          }
        }
      }

      if (baseline.scenarioId) {
        const baselineContexts = parallelRun.contexts.filter(
          (context) => context.scenarioId === baseline.scenarioId,
        );
        for (const context of baselineContexts) {
          if (!valuesEqual(baseline.result, context.result)) {
            violations.push({
              scenarioId: scenario.scenarioId,
              kind: "parallel-baseline-divergence",
              fact: "parallel execution diverged from baseline scenario result",
              details: {
                parallelId: parallelRun.parallelId,
                contextId: context.contextId,
                baselineResult: sanitizeForEvidence(baseline.result),
                observedResult: sanitizeForEvidence(context.result),
              },
            });
          }
        }
      }
    }
  }

  for (const leak of scenario.leakedResources ?? []) {
    violations.push({
      scenarioId: scenario.scenarioId,
      kind: "execution-context-leak",
      fact: `${leak.kind} leaked across execution contexts`,
      details: sanitizeForEvidence(leak),
    });
  }

  return violations;
}

function isLiveDependency(
  classification: ModeIsolationScenario["accessAttempts"][number]["classification"],
): boolean {
  return classification === "live" || classification === "production";
}

export function detectUni073Violations(scenario: IsolationScenario): IsolationViolation[] {
  if (scenario.kind !== "mode-isolation") {
    return [];
  }

  if (!scenario.isolationContract.declaresIsolation) {
    return [];
  }

  const violations: IsolationViolation[] = [];

  for (const attempt of scenario.accessAttempts) {
    if (!attempt.reached || !isLiveDependency(attempt.classification)) {
      continue;
    }

    violations.push({
      scenarioId: scenario.scenarioId,
      kind: "live-dependency-reached",
      fact: `${scenario.mode} mode with declared isolation reached live dependency`,
      details: sanitizeForEvidence({
        mode: scenario.mode,
        target: attempt.target,
        classification: attempt.classification,
        channel: attempt.channel,
        allowedDependencyTargets: scenario.isolationContract.allowedDependencyTargets,
      }),
    });
  }

  return violations;
}
