import { utf8ByteLength } from "../execution/request-limits";
import {
  MINIMIZER_MAXIMUM_HISTORY_ENTRIES,
  type AcceptedSubjectTransform,
  type AcceptedTransformKind,
  type MinimizationCandidateObservation,
  type MinimizationPhase,
  type SubjectMinimizationBudgets,
  type SubjectMinimizationProgress,
  type SubjectMinimizationResult,
  type SubjectMinimizationStopReason,
} from "./minimization.types";

const CANONICAL_SCALARS = ["a", "0", " ", "\n", "!"] as const;

export type CandidateEvaluator = (
  subject: string,
) => Promise<MinimizationCandidateObservation>;

export interface SubjectReducerOptions {
  readonly subject: string;
  readonly targetIdentity: string;
  readonly budgets: SubjectMinimizationBudgets;
  readonly evaluate: CandidateEvaluator;
  readonly now: () => number;
  readonly startedAtMs?: number;
  readonly isCancelled?: () => boolean;
  readonly onProgress?: (progress: SubjectMinimizationProgress) => void;
}

type EvaluationAttempt =
  | {
      readonly kind: "observation";
      readonly observation: MinimizationCandidateObservation;
    }
  | {
      readonly kind: "stop";
      readonly reason: SubjectMinimizationStopReason;
      readonly observation?: MinimizationCandidateObservation;
    };

const NOT_RUN: MinimizationCandidateObservation = {
  status: "inconclusive",
  reproduced: false,
  fingerprint: ["not-run"],
  summary: "No engine candidate could start within the configured budgets.",
};

export function containsLoneSurrogate(value: string): boolean {
  for (let index = 0; index < value.length; index += 1) {
    const first = value.charCodeAt(index);
    if (first >= 0xd800 && first <= 0xdbff) {
      const second = value.charCodeAt(index + 1);
      if (!(second >= 0xdc00 && second <= 0xdfff)) return true;
      index += 1;
    } else if (first >= 0xdc00 && first <= 0xdfff) {
      return true;
    }
  }
  return false;
}

export function unicodeScalars(value: string): readonly string[] {
  if (containsLoneSurrogate(value)) {
    throw new Error(
      "Subject minimization requires well-formed Unicode and will not create or preserve lone surrogate code units.",
    );
  }
  return Array.from(value);
}

function resultStatus(
  reason: SubjectMinimizationStopReason,
): SubjectMinimizationResult["status"] {
  if (reason === "locally-minimal") return "complete";
  if (reason === "cancelled") return "cancelled";
  if (reason === "baseline-not-reproduced" || reason === "engine-failure") {
    return "unsupported";
  }
  return "partial";
}

function stopForObservation(
  observation: MinimizationCandidateObservation,
): SubjectMinimizationStopReason | undefined {
  if (observation.status === "cancelled") return "cancelled";
  if (observation.status === "crash" || observation.status === "worker-error") {
    return "engine-failure";
  }
  return undefined;
}

function canonicalRank(value: string): number {
  const rank = CANONICAL_SCALARS.indexOf(
    value as (typeof CANONICAL_SCALARS)[number],
  );
  return rank < 0 ? Number.POSITIVE_INFINITY : rank;
}

/**
 * Deterministic subject reducer.
 *
 * It first performs contiguous, left-to-right ddmin chunk deletion. It then
 * reaches a fixed point over single-scalar deletion followed by lower-rank
 * canonical scalar replacement. No claim is made beyond local minimality for
 * that documented transform set.
 */
export async function reduceSubject(
  options: SubjectReducerOptions,
): Promise<SubjectMinimizationResult> {
  const originalScalars = unicodeScalars(options.subject);
  const start = options.startedAtMs ?? options.now();
  let current = [...originalScalars];
  let evaluations = 0;
  let acceptedReductions = 0;
  let phase: MinimizationPhase = "baseline";
  let lastObservation: MinimizationCandidateObservation | undefined;
  let baseline: MinimizationCandidateObservation | undefined;
  let finalObservation: MinimizationCandidateObservation | undefined;
  let stopReason: SubjectMinimizationStopReason | undefined;
  const history: AcceptedSubjectTransform[] = [];
  let historyTruncated = false;

  const elapsed = () => Math.max(0, options.now() - start);
  const emit = () => {
    options.onProgress?.({
      phase,
      evaluations,
      maximumEvaluations: options.budgets.maximumEvaluations,
      acceptedReductions,
      currentScalars: current.length,
      currentUtf8Bytes: utf8ByteLength(current.join("")),
      elapsedMs: elapsed(),
      maximumWallTimeMs: options.budgets.maximumWallTimeMs,
      ...(lastObservation ? { lastObservation } : {}),
    });
  };
  const attempt = async (
    candidate: readonly string[],
  ): Promise<EvaluationAttempt> => {
    if (options.isCancelled?.()) return { kind: "stop", reason: "cancelled" };
    if (evaluations >= options.budgets.maximumEvaluations) {
      return { kind: "stop", reason: "evaluation-budget-exhausted" };
    }
    if (
      elapsed() + options.budgets.candidateTimeoutMs >
      options.budgets.maximumWallTimeMs
    ) {
      return { kind: "stop", reason: "wall-time-budget-exhausted" };
    }

    const observation = await options.evaluate(candidate.join(""));
    evaluations += 1;
    lastObservation = observation;
    emit();

    if (options.isCancelled?.()) {
      return { kind: "stop", reason: "cancelled", observation };
    }
    const observationStop = stopForObservation(observation);
    if (observationStop) {
      return {
        kind: "stop",
        reason: observationStop,
        observation,
      };
    }
    if (elapsed() > options.budgets.maximumWallTimeMs) {
      return {
        kind: "stop",
        reason: "wall-time-budget-exhausted",
        observation,
      };
    }
    return { kind: "observation", observation };
  };
  const retainTransform = (
    kind: AcceptedTransformKind,
    detail: string,
    before: readonly string[],
    after: readonly string[],
  ) => {
    acceptedReductions += 1;
    if (history.length < MINIMIZER_MAXIMUM_HISTORY_ENTRIES) {
      history.push({
        kind,
        detail,
        beforeScalars: before.length,
        afterScalars: after.length,
        beforeUtf8Bytes: utf8ByteLength(before.join("")),
        afterUtf8Bytes: utf8ByteLength(after.join("")),
      });
    } else {
      historyTruncated = true;
    }
    emit();
  };
  const acceptCandidate = (
    attemptResult: Extract<EvaluationAttempt, { readonly kind: "observation" }>,
    candidate: readonly string[],
    kind: AcceptedTransformKind,
    detail: string,
  ): boolean => {
    if (!attemptResult.observation.reproduced) return false;
    const before = current;
    current = [...candidate];
    finalObservation = attemptResult.observation;
    retainTransform(kind, detail, before, current);
    return true;
  };

  emit();
  const baselineAttempt = await attempt(current);
  if (baselineAttempt.kind === "stop") {
    baseline = baselineAttempt.observation ?? NOT_RUN;
    finalObservation = baseline;
    stopReason = baselineAttempt.reason;
  } else {
    baseline = baselineAttempt.observation;
    finalObservation = baseline;
    if (!baseline.reproduced) {
      stopReason = "baseline-not-reproduced";
    }
  }

  if (!stopReason) {
    phase = "chunk-deletion";
    let granularity = 2;
    while (current.length >= 2 && !stopReason) {
      const chunkLength = Math.ceil(current.length / granularity);
      let accepted = false;
      for (
        let startIndex = 0;
        startIndex < current.length;
        startIndex += chunkLength
      ) {
        const endIndex = Math.min(current.length, startIndex + chunkLength);
        const candidate = [
          ...current.slice(0, startIndex),
          ...current.slice(endIndex),
        ];
        const candidateAttempt = await attempt(candidate);
        if (candidateAttempt.kind === "stop") {
          stopReason = candidateAttempt.reason;
          break;
        }
        if (candidateAttempt.observation.status === "inconclusive") {
          stopReason = "inconclusive-candidate";
          break;
        }
        if (
          acceptCandidate(
            candidateAttempt,
            candidate,
            "chunk-deletion",
            `Removed scalar interval ${startIndex}–${endIndex}.`,
          )
        ) {
          granularity = Math.max(2, granularity - 1);
          accepted = true;
          break;
        }
      }
      if (stopReason) break;
      if (accepted) continue;
      if (granularity >= current.length) break;
      granularity = Math.min(current.length, granularity * 2);
    }
  }

  if (!stopReason) {
    phase = "local-sweep";
    let changed = true;
    while (changed && !stopReason) {
      changed = false;

      for (let index = 0; index < current.length; index += 1) {
        const candidate = [
          ...current.slice(0, index),
          ...current.slice(index + 1),
        ];
        const candidateAttempt = await attempt(candidate);
        if (candidateAttempt.kind === "stop") {
          stopReason = candidateAttempt.reason;
          break;
        }
        if (candidateAttempt.observation.status === "inconclusive") {
          stopReason = "inconclusive-candidate";
          break;
        }
        if (
          acceptCandidate(
            candidateAttempt,
            candidate,
            "single-scalar-deletion",
            `Removed scalar ${index}.`,
          )
        ) {
          changed = true;
          break;
        }
      }
      if (stopReason || changed) continue;

      for (let index = 0; index < current.length; index += 1) {
        const present = current[index];
        if (present === undefined) continue;
        const rank = canonicalRank(present);
        for (
          let replacementRank = 0;
          replacementRank < rank && replacementRank < CANONICAL_SCALARS.length;
          replacementRank += 1
        ) {
          const replacement = CANONICAL_SCALARS[replacementRank];
          if (replacement === undefined || replacement === present) continue;
          const candidate = [...current];
          candidate[index] = replacement;
          const candidateAttempt = await attempt(candidate);
          if (candidateAttempt.kind === "stop") {
            stopReason = candidateAttempt.reason;
            break;
          }
          if (candidateAttempt.observation.status === "inconclusive") {
            stopReason = "inconclusive-candidate";
            break;
          }
          if (
            acceptCandidate(
              candidateAttempt,
              candidate,
              "scalar-simplification",
              `Replaced scalar ${index} with ${JSON.stringify(replacement)}.`,
            )
          ) {
            changed = true;
            break;
          }
        }
        if (stopReason || changed) break;
      }
    }
    if (!stopReason) stopReason = "locally-minimal";
  }

  phase = "complete";
  emit();
  const reason = stopReason ?? "engine-failure";
  const minimizedSubject = current.join("");
  return {
    schemaVersion: 1,
    targetIdentity: options.targetIdentity,
    status: resultStatus(reason),
    stopReason: reason,
    locallyMinimal: reason === "locally-minimal",
    originalSubject: options.subject,
    minimizedSubject,
    originalScalars: originalScalars.length,
    minimizedScalars: current.length,
    originalUtf8Bytes: utf8ByteLength(options.subject),
    minimizedUtf8Bytes: utf8ByteLength(minimizedSubject),
    evaluations,
    acceptedReductions,
    elapsedMs: elapsed(),
    budgets: options.budgets,
    baseline: baseline ?? NOT_RUN,
    final: finalObservation ?? baseline ?? NOT_RUN,
    retainedHistory: history,
    historyTruncated,
  };
}
