import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
  CaseGenerationOrchestrator,
  type CaseGenerationClient,
} from "../regex/generation/CaseGenerationOrchestrator";
import {
  DEFAULT_GENERATION_SETTINGS,
  GENERATION_LIMITS,
} from "../regex/generation/generation-limits";
import type {
  CaseGenerationProgress,
  CaseGenerationResult,
  CandidateGenerationSettings,
  VerifiedGeneratedCase,
} from "../regex/generation/generation.types";
import {
  GENERATED_CASE_GENERATOR_ID,
  GENERATED_CASE_GENERATOR_VERSION,
} from "../regex/generation/generation.types";
import { DEFAULT_REGEX_LIMITS } from "../regex/execution/request-limits";
import { WorkerRequestError } from "../regex/execution/WorkerSupervisor";
import type {
  RegexEngineOptions,
  RegexFlavourId,
} from "../regex/model/flavour";
import type { RegexSyntaxResult, SourceRange } from "../regex/model/syntax";
import "./GenerationPanel.css";

const MAXIMUM_SUBJECT_PREVIEW_UTF16 = 240;

function defaultOrchestrator(): CaseGenerationClient {
  return new CaseGenerationOrchestrator();
}

function integer(
  source: string,
  label: string,
  minimum: number,
  maximum: number,
): number {
  const value = Number(source);
  if (
    source.trim() === "" ||
    !Number.isSafeInteger(value) ||
    value < minimum ||
    value > maximum
  ) {
    throw new RangeError(
      `${label} must be a whole number from ${minimum.toLocaleString()} to ${maximum.toLocaleString()}.`,
    );
  }
  return value;
}

function subjectPreview(subject: string): string {
  if (subject.length <= MAXIMUM_SUBJECT_PREVIEW_UTF16) {
    return JSON.stringify(subject);
  }
  return `${JSON.stringify(subject.slice(0, MAXIMUM_SUBJECT_PREVIEW_UTF16))}… (${subject.length.toLocaleString()} UTF-16 units)`;
}

function downloadCases(
  result: CaseGenerationResult,
  configuration: {
    readonly flavour: RegexFlavourId;
    readonly flavourVersion?: string;
    readonly pattern: string;
    readonly flags: readonly string[];
    readonly options: RegexEngineOptions;
    readonly scanAll: boolean;
  },
): void {
  const payload = {
    schemaVersion: 1,
    kind: "regex-tools-generated-cases",
    generator: {
      id: GENERATED_CASE_GENERATOR_ID,
      version: GENERATED_CASE_GENERATOR_VERSION,
      seed: result.seed,
      seedHash: result.seedHash,
    },
    configuration,
    engine: result.engine,
    cases: result.cases.map((candidate) => ({
      id: candidate.id,
      name: candidate.name,
      subject: candidate.subject,
      expectation: { kind: candidate.expectation },
      features: candidate.features,
      provenance: candidate.provenance,
    })),
  };
  const url = URL.createObjectURL(
    new Blob([`${JSON.stringify(payload, null, 2)}\n`], {
      type: "application/json",
    }),
  );
  const anchor = document.createElement("a");
  anchor.href = url;
  anchor.download = `regex-generated-${result.seedHash}.json`;
  anchor.click();
  globalThis.setTimeout(() => URL.revokeObjectURL(url), 1_000);
}

export interface GenerationPanelProps {
  readonly active: boolean;
  readonly flavour: RegexFlavourId;
  readonly flavourVersion?: string;
  readonly pattern: string;
  readonly flags: readonly string[];
  readonly options: RegexEngineOptions;
  readonly scanAll: boolean;
  readonly syntax?: RegexSyntaxResult;
  readonly onClose?: () => void;
  readonly onAddToTests?: (cases: readonly VerifiedGeneratedCase[]) => void;
  readonly onSelectPatternRange?: (range: SourceRange) => void;
  readonly createOrchestrator?: () => CaseGenerationClient;
}

export function GenerationPanel({
  active,
  flavour,
  flavourVersion,
  pattern,
  flags,
  options,
  scanAll,
  syntax,
  onClose,
  onAddToTests,
  onSelectPatternRange,
  createOrchestrator = defaultOrchestrator,
}: GenerationPanelProps) {
  const [seed, setSeed] = useState("case-seed-1");
  const [maximumCases, setMaximumCases] = useState(
    String(DEFAULT_GENERATION_SETTINGS.maximumCases),
  );
  const [maximumCandidateAttempts, setMaximumCandidateAttempts] = useState(
    String(DEFAULT_GENERATION_SETTINGS.maximumCandidateAttempts),
  );
  const [randomVariantCount, setRandomVariantCount] = useState(
    String(DEFAULT_GENERATION_SETTINGS.randomVariantCount),
  );
  const [maximumQuantifierRepetitions, setMaximumQuantifierRepetitions] =
    useState(String(DEFAULT_GENERATION_SETTINGS.maximumQuantifierRepetitions));
  const [maximumSubjectBytes, setMaximumSubjectBytes] = useState(
    String(DEFAULT_GENERATION_SETTINGS.maximumSubjectBytes),
  );
  const [maximumTotalSubjectBytes, setMaximumTotalSubjectBytes] = useState(
    String(DEFAULT_GENERATION_SETTINGS.maximumTotalSubjectBytes),
  );
  const [perCaseTimeoutMs, setPerCaseTimeoutMs] = useState(
    String(DEFAULT_GENERATION_SETTINGS.perCaseTimeoutMs),
  );
  const [maximumWallTimeMs, setMaximumWallTimeMs] = useState(
    String(DEFAULT_GENERATION_SETTINGS.maximumWallTimeMs),
  );
  const [running, setRunning] = useState(false);
  const [progress, setProgress] = useState<CaseGenerationProgress>();
  const [message, setMessage] = useState(
    "No cases have been generated for this configuration.",
  );
  const [error, setError] = useState<string>();
  const [resultRecord, setResultRecord] = useState<{
    readonly configurationKey: symbol;
    readonly result: CaseGenerationResult;
  }>();
  const [selectedIds, setSelectedIds] = useState<ReadonlySet<string>>(
    new Set(),
  );
  const orchestrator = useRef<CaseGenerationClient | undefined>(undefined);
  const runRevision = useRef(0);

  const syntaxCurrent =
    syntax?.accepted === true &&
    syntax.root.raw === pattern &&
    syntax.root.support.flavour === flavour;
  const generationAvailable = flavour === "ecmascript" && syntaxCurrent;
  const configurationKey = useMemo(
    () =>
      Symbol(
        `generation-${flavour}-${flavourVersion ?? ""}-${flags.join("")}-${JSON.stringify(options)}-${pattern.length}-${String(scanAll)}`,
      ),
    [flags, flavour, flavourVersion, options, pattern, scanAll],
  );
  const previousConfiguration = useRef(configurationKey);
  const result =
    resultRecord?.configurationKey === configurationKey
      ? resultRecord.result
      : undefined;

  const disposeActive = useCallback(() => {
    orchestrator.current?.cancel();
    orchestrator.current?.dispose();
    orchestrator.current = undefined;
  }, []);

  useEffect(() => {
    if (active) return;
    runRevision.current += 1;
    disposeActive();
    queueMicrotask(() => setRunning(false));
  }, [active, disposeActive]);

  useEffect(() => {
    if (previousConfiguration.current === configurationKey) return;
    previousConfiguration.current = configurationKey;
    const revision = ++runRevision.current;
    disposeActive();
    queueMicrotask(() => {
      if (runRevision.current !== revision) return;
      setRunning(false);
      setProgress(undefined);
      setResultRecord(undefined);
      setSelectedIds(new Set());
      setError(undefined);
      setMessage(
        "Pattern or engine configuration changed; previous generated cases were cleared.",
      );
    });
  }, [configurationKey, disposeActive]);

  useEffect(
    () => () => {
      runRevision.current += 1;
      disposeActive();
    },
    [disposeActive],
  );

  const settings = (): CandidateGenerationSettings => {
    const retained = integer(
      maximumCases,
      "Maximum retained cases",
      1,
      DEFAULT_REGEX_LIMITS.maximumGeneratedCases,
    );
    return {
      maximumCases: retained,
      maximumCandidateAttempts: integer(
        maximumCandidateAttempts,
        "Maximum candidate attempts",
        retained,
        GENERATION_LIMITS.maximumCandidateAttempts,
      ),
      maximumTotalSubjectBytes: integer(
        maximumTotalSubjectBytes,
        "Maximum aggregate subject bytes",
        1,
        GENERATION_LIMITS.maximumTotalSubjectBytes,
      ),
      maximumSubjectBytes: integer(
        maximumSubjectBytes,
        "Maximum bytes per subject",
        1,
        GENERATION_LIMITS.maximumSubjectBytes,
      ),
      maximumAstNodes: DEFAULT_GENERATION_SETTINGS.maximumAstNodes,
      maximumAstDepth: DEFAULT_GENERATION_SETTINGS.maximumAstDepth,
      maximumQuantifierRepetitions: integer(
        maximumQuantifierRepetitions,
        "Maximum synthesized repetitions",
        1,
        GENERATION_LIMITS.maximumQuantifierRepetitions,
      ),
      randomVariantCount: integer(
        randomVariantCount,
        "Random variants",
        0,
        DEFAULT_REGEX_LIMITS.maximumGeneratedCases,
      ),
      perCaseTimeoutMs: integer(
        perCaseTimeoutMs,
        "Per-case timeout",
        GENERATION_LIMITS.minimumPerCaseTimeoutMs,
        GENERATION_LIMITS.maximumPerCaseTimeoutMs,
      ),
      maximumWallTimeMs: integer(
        maximumWallTimeMs,
        "Aggregate wall time",
        1,
        GENERATION_LIMITS.maximumWallTimeMs,
      ),
    };
  };

  const generate = async () => {
    if (!generationAvailable || !syntax) return;
    const revision = ++runRevision.current;
    disposeActive();
    const client = createOrchestrator();
    orchestrator.current = client;
    setRunning(true);
    setProgress(undefined);
    setError(undefined);
    setResultRecord(undefined);
    setSelectedIds(new Set());
    setMessage(
      "Starting deterministic synthesis and actual-engine verification…",
    );
    try {
      const next = await client.run(
        {
          flavour,
          ...(flavourVersion === undefined ? {} : { flavourVersion }),
          pattern,
          flags,
          options,
          scanAll,
          root: syntax.root,
          captureMetadata: syntax.captures,
          syntaxAccepted: syntax.accepted,
          seed,
          settings: settings(),
        },
        (value) => {
          if (runRevision.current !== revision) return;
          setProgress(value);
          setMessage(value.message);
        },
      );
      if (runRevision.current !== revision) return;
      setResultRecord({ configurationKey, result: next });
      setSelectedIds(new Set(next.cases.map((candidate) => candidate.id)));
      setMessage(
        next.stoppedReason ??
          `Generation ${next.status}: ${next.cases.length.toLocaleString()} actual-engine-verified cases retained.`,
      );
    } catch (cause) {
      if (runRevision.current !== revision) return;
      if (cause instanceof WorkerRequestError && cause.kind === "cancelled") {
        setMessage("Generation cancelled; its workers were terminated.");
      } else {
        const detail =
          cause instanceof Error
            ? cause.message
            : "Generated cases could not be produced.";
        setError(detail);
        setMessage("Generation settings or worker execution require review.");
      }
    } finally {
      client.dispose();
      if (orchestrator.current === client) orchestrator.current = undefined;
      if (runRevision.current === revision) {
        setRunning(false);
        setProgress(undefined);
      }
    }
  };

  const cancel = () => {
    orchestrator.current?.cancel();
    setMessage(
      "Cancelling synthesis and verification; active workers are being terminated…",
    );
  };

  const selectedCases =
    result?.cases.filter((candidate) => selectedIds.has(candidate.id)) ?? [];
  const progressMaximum = Math.max(1, progress?.total ?? 1);
  const progressValue = Math.min(progressMaximum, progress?.completed ?? 0);

  return (
    <section
      className="panel generation-panel"
      aria-labelledby="generation-heading"
    >
      <header className="panel-heading">
        <div>
          <p className="eyebrow">
            Normalized AST candidates · actual-engine truth
          </p>
          <h2 id="generation-heading">Generated test cases</h2>
        </div>
        <div className="panel-heading-actions">
          <span className="provenance-badge">
            {generationAvailable
              ? "ECMAScript generator v1"
              : "Coverage unavailable"}
          </span>
          {onClose ? (
            <button
              type="button"
              className="icon-button"
              aria-label="Close generated test cases"
              data-dialog-initial-focus
              onClick={onClose}
            >
              ×
            </button>
          ) : null}
        </div>
      </header>

      <p className="generation-disclaimer">
        Generation samples supported structure; it is not proof of complete
        coverage. A positive or negative label appears only after the selected
        actual engine confirms that exact subject. Rejected candidates remain
        diagnostics, never tests.
      </p>

      {!generationAvailable ? (
        <div className="generation-unavailable" role="status">
          <strong>
            Generated cases are unavailable for this configuration.
          </strong>
          <span>
            {flavour === "pcre2"
              ? "The current PCRE2 syntax provider is intentionally partial and does not expose a complete normalized AST. It is not reinterpreted as ECMAScript."
              : syntaxCurrent
                ? "This flavour does not have an implemented generator."
                : "Wait for an accepted, current syntax result before generating."}
          </span>
        </div>
      ) : (
        <form
          className="generation-settings"
          onSubmit={(event) => {
            event.preventDefault();
            void generate();
          }}
        >
          <div className="generation-settings-primary">
            <label>
              <span>Deterministic seed</span>
              <input
                value={seed}
                maxLength={GENERATION_LIMITS.maximumSeedUtf16}
                disabled={running}
                required
                onChange={(event) => setSeed(event.target.value)}
              />
            </label>
            <label>
              <span>Retain at most</span>
              <input
                type="number"
                min={1}
                max={DEFAULT_REGEX_LIMITS.maximumGeneratedCases}
                value={maximumCases}
                disabled={running}
                onChange={(event) => setMaximumCases(event.target.value)}
              />
            </label>
            <label>
              <span>Seeded variants</span>
              <input
                type="number"
                min={0}
                max={DEFAULT_REGEX_LIMITS.maximumGeneratedCases}
                value={randomVariantCount}
                disabled={running}
                onChange={(event) => setRandomVariantCount(event.target.value)}
              />
            </label>
          </div>
          <details>
            <summary>Advanced generation bounds</summary>
            <div className="generation-settings-grid">
              <label>
                <span>Candidate attempts</span>
                <input
                  type="number"
                  min={1}
                  max={GENERATION_LIMITS.maximumCandidateAttempts}
                  value={maximumCandidateAttempts}
                  disabled={running}
                  onChange={(event) =>
                    setMaximumCandidateAttempts(event.target.value)
                  }
                />
              </label>
              <label>
                <span>Per-case timeout (ms)</span>
                <input
                  type="number"
                  min={GENERATION_LIMITS.minimumPerCaseTimeoutMs}
                  max={GENERATION_LIMITS.maximumPerCaseTimeoutMs}
                  value={perCaseTimeoutMs}
                  disabled={running}
                  onChange={(event) => setPerCaseTimeoutMs(event.target.value)}
                />
              </label>
              <label>
                <span>Aggregate wall time (ms)</span>
                <input
                  type="number"
                  min={1}
                  max={GENERATION_LIMITS.maximumWallTimeMs}
                  value={maximumWallTimeMs}
                  disabled={running}
                  onChange={(event) => setMaximumWallTimeMs(event.target.value)}
                />
              </label>
              <label>
                <span>Bytes per subject</span>
                <input
                  type="number"
                  min={1}
                  max={GENERATION_LIMITS.maximumSubjectBytes}
                  value={maximumSubjectBytes}
                  disabled={running}
                  onChange={(event) =>
                    setMaximumSubjectBytes(event.target.value)
                  }
                />
              </label>
              <label>
                <span>Aggregate subject bytes</span>
                <input
                  type="number"
                  min={1}
                  max={GENERATION_LIMITS.maximumTotalSubjectBytes}
                  value={maximumTotalSubjectBytes}
                  disabled={running}
                  onChange={(event) =>
                    setMaximumTotalSubjectBytes(event.target.value)
                  }
                />
              </label>
              <label>
                <span>Synthesized repetitions</span>
                <input
                  type="number"
                  min={1}
                  max={GENERATION_LIMITS.maximumQuantifierRepetitions}
                  value={maximumQuantifierRepetitions}
                  disabled={running}
                  onChange={(event) =>
                    setMaximumQuantifierRepetitions(event.target.value)
                  }
                />
              </label>
            </div>
          </details>
          <div className="generation-run-actions">
            {running ? (
              <button type="button" className="primary-button" onClick={cancel}>
                Cancel generation
              </button>
            ) : (
              <button type="submit" className="primary-button">
                Generate &amp; verify
              </button>
            )}
            <small>
              Hard caps:{" "}
              {DEFAULT_REGEX_LIMITS.maximumGeneratedCases.toLocaleString()}{" "}
              retained cases ·{" "}
              {GENERATION_LIMITS.maximumTotalSubjectBytes / 1024 / 1024} MiB
              candidate text · {GENERATION_LIMITS.maximumWallTimeMs / 1_000} s
              wall time
            </small>
          </div>
        </form>
      )}

      <div
        className={`generation-status ${error ? "is-error" : ""}`}
        role={error ? "alert" : "status"}
        aria-live="polite"
      >
        <strong>{message}</strong>
        {progress ? (
          <>
            <progress
              aria-label="Generated-case progress"
              max={progressMaximum}
              value={progressValue}
            />
            <span>
              {progress.phase} · {progress.completed.toLocaleString()} /{" "}
              {progress.total.toLocaleString()} ·{" "}
              {progress.retained.toLocaleString()} retained
            </span>
          </>
        ) : null}
        {error ? <span>{error}</span> : null}
      </div>

      {result ? (
        <div className="generation-results">
          <section
            className="generation-summary"
            aria-label="Generation summary"
          >
            <dl>
              <div>
                <dt>Status</dt>
                <dd>{result.status.replaceAll("-", " ")}</dd>
              </div>
              <div>
                <dt>Seed</dt>
                <dd>
                  <code>{result.seed}</code> · {result.seedHash}
                </dd>
              </div>
              <div>
                <dt>Actual engine</dt>
                <dd>
                  {result.engine
                    ? `${result.engine.engineName} · ${result.engine.engineVersion}`
                    : "No candidate reached engine verification"}
                </dd>
              </div>
              <div>
                <dt>Verified cases</dt>
                <dd>{result.cases.length.toLocaleString()}</dd>
              </div>
              <div>
                <dt>Engine executions</dt>
                <dd>{result.engineExecutions.toLocaleString()}</dd>
              </div>
              <div>
                <dt>Candidate bytes</dt>
                <dd>{result.generatedCandidateBytes.toLocaleString()}</dd>
              </div>
              <div>
                <dt>Wall time</dt>
                <dd>{result.wallTimeMs.toFixed(1)} ms</dd>
              </div>
            </dl>
            {result.warnings.length > 0 ? (
              <ul className="generation-warnings">
                {result.warnings.map((warning) => (
                  <li key={warning}>{warning}</li>
                ))}
              </ul>
            ) : null}
          </section>

          <section
            className="generation-coverage"
            aria-labelledby="generation-coverage-heading"
          >
            <header>
              <div>
                <p className="eyebrow">Claimed only for generator v1</p>
                <h3 id="generation-coverage-heading">Coverage report</h3>
              </div>
            </header>
            <ul>
              {result.coverage.map((entry) => (
                <li key={entry.feature}>
                  <span className={`status status-${entry.status}`}>
                    {entry.status}
                  </span>
                  <strong>{entry.feature.replaceAll("-", " ")}</strong>
                  <span>{entry.detail}</span>
                </li>
              ))}
            </ul>
            {result.unsupportedConstructs.length > 0 ? (
              <details>
                <summary>
                  {result.unsupportedConstructs.length.toLocaleString()}{" "}
                  unsupported or partially synthesized construct(s)
                </summary>
                <ul className="unsupported-generation-list">
                  {result.unsupportedConstructs.map((construct) => (
                    <li key={construct.id}>
                      <button
                        type="button"
                        className="text-button"
                        disabled={!onSelectPatternRange}
                        onClick={() => onSelectPatternRange?.(construct.range)}
                      >
                        {construct.kind} · {construct.range.startUtf16}–
                        {construct.range.endUtf16}
                      </button>
                      <code>{construct.rawPreview || "empty"}</code>
                      <span>{construct.reason}</span>
                    </li>
                  ))}
                </ul>
              </details>
            ) : null}
          </section>

          <section
            className="generation-cases"
            aria-labelledby="generated-cases-heading"
          >
            <header>
              <div>
                <p className="eyebrow">Exact verified subjects</p>
                <h3 id="generated-cases-heading">
                  Retained cases{" "}
                  <small>({result.cases.length.toLocaleString()})</small>
                </h3>
              </div>
              <div className="generation-case-actions">
                <button
                  type="button"
                  className="secondary-button"
                  disabled={result.cases.length === 0}
                  onClick={() =>
                    setSelectedIds(
                      selectedIds.size === result.cases.length
                        ? new Set()
                        : new Set(
                            result.cases.map((candidate) => candidate.id),
                          ),
                    )
                  }
                >
                  {selectedIds.size === result.cases.length
                    ? "Select none"
                    : "Select all"}
                </button>
                <button
                  type="button"
                  className="secondary-button"
                  disabled={result.cases.length === 0}
                  onClick={() =>
                    downloadCases(result, {
                      flavour,
                      ...(flavourVersion === undefined
                        ? {}
                        : { flavourVersion }),
                      pattern,
                      flags,
                      options,
                      scanAll,
                    })
                  }
                >
                  Export verified JSON
                </button>
                {onAddToTests ? (
                  <button
                    type="button"
                    className="primary-button"
                    disabled={selectedCases.length === 0}
                    onClick={() => {
                      try {
                        onAddToTests(selectedCases);
                        setError(undefined);
                        setMessage(
                          `Added ${selectedCases.length.toLocaleString()} generated cases to the unit-test suite with seed provenance.`,
                        );
                      } catch (cause) {
                        setError(
                          cause instanceof Error
                            ? cause.message
                            : "Generated cases could not be added.",
                        );
                      }
                    }}
                  >
                    Add selected to tests
                  </button>
                ) : null}
              </div>
            </header>
            {result.cases.length === 0 ? (
              <p className="empty-state">
                No candidate satisfied its intended property within the
                configured bounds.
              </p>
            ) : (
              <ul className="generated-case-list">
                {result.cases.map((candidate) => (
                  <li key={candidate.id}>
                    <label>
                      <input
                        type="checkbox"
                        aria-label={`Select ${candidate.name}`}
                        checked={selectedIds.has(candidate.id)}
                        onChange={(event) => {
                          const checked = event.target.checked;
                          setSelectedIds((current) => {
                            const next = new Set(current);
                            if (checked) next.add(candidate.id);
                            else next.delete(candidate.id);
                            return next;
                          });
                        }}
                      />
                      <span>
                        <strong>{candidate.name}</strong>
                        <small>
                          {candidate.expectation.replaceAll("-", " ")} ·{" "}
                          {candidate.subjectBytes.toLocaleString()} bytes ·{" "}
                          {candidate.elapsedMs.toFixed(2)} ms
                        </small>
                      </span>
                    </label>
                    <code>{subjectPreview(candidate.subject)}</code>
                    <div className="generation-feature-chips">
                      {candidate.features.map((feature) => (
                        <span key={feature}>
                          {feature.replaceAll("-", " ")}
                        </span>
                      ))}
                      <span>actual engine verified</span>
                    </div>
                  </li>
                ))}
              </ul>
            )}
          </section>

          {result.discarded.length > 0 ? (
            <details className="generation-discarded">
              <summary>
                Advanced: {result.discarded.length.toLocaleString()} rejected
                candidate diagnostic(s)
              </summary>
              <p>
                These candidates are not tests. Subjects are clipped here; the
                diagnostic records only why each intended property failed.
              </p>
              <ul>
                {result.discarded.map((candidate) => (
                  <li key={`${candidate.candidateId}-${candidate.reason}`}>
                    <strong>{candidate.reason.replaceAll("-", " ")}</strong>
                    <code>{subjectPreview(candidate.subjectPreview)}</code>
                    <span>{candidate.detail}</span>
                  </li>
                ))}
              </ul>
            </details>
          ) : null}
        </div>
      ) : null}
    </section>
  );
}
