import type { BooleanResult, CaptureOptions, CaptureResult, ClickResult, AutomationGuard, ScrollResult } from "../types/global";
import type { ParsedArtifactCandidate } from "./artifactOcrParser";
import { sessionSignature } from "./artifactStore";
import { buildGridModel, buildInventoryPagePlan, type GridTarget } from "./automationPlanner";
import { classifyAutoScanCapture, shouldAbortAfterConsecutiveMisses } from "./autoScanController";
import { waitForCardReady } from "./cardReadyGate";
import { captureRejectionReason, captureSourceRejectionReason, shouldPersistParsedArtifact } from "./scannerCaptureQuality";
import type { AutoScanStats, ScanSummary } from "./scannerSession";
import { addCaptureTiming, addCardReadyTiming, addScrollReadyTiming, clampSkipRows, emptyAutoScanStats, resolveScanTargetCount, updateScanTiming } from "./scannerSession";
import { validateAutoScanEntryPreflight } from "./autoScanEntry";

// One click per tile, fingerprint verification, one retry if the detail view did
// not change, then move on. Extra click profiles made failures harder to
// diagnose and did not improve the validated visible-inventory path.

type AutoScanApi = {
  clickScreen: (x: number, y: number) => Promise<ClickResult>;
  scrollScreen: (notches: number, anchorX?: number, anchorY?: number) => Promise<ScrollResult>;
  getAutomationGuard?: () => Promise<AutomationGuard>;
};

export type AutoScanLoopDependencies = {
  api: AutoScanApi;
  captureSelectedSource: (delayMs?: number, focusGenshin?: boolean, options?: CaptureOptions) => Promise<CaptureResult | null>;
  captureFastSelectedSource: (delayMs?: number, focusGenshin?: boolean, options?: CaptureOptions) => Promise<CaptureResult | null>;
  parseArtifact: (capture: CaptureResult | null) => ParsedArtifactCandidate | null;
  persistParsedArtifact: (
    capture: CaptureResult | null,
    parsed: ParsedArtifactCandidate,
    source: string,
    needsReview: boolean,
  ) => Promise<boolean>;
  persistParsedArtifactsBatch?: (
    items: Array<{
      capture: CaptureResult | null;
      parsed: ParsedArtifactCandidate;
      source: string;
      needsReview: boolean;
    }>,
  ) => Promise<number>;
  saveReviewSample: (
    capture: CaptureResult | null,
    parsed: ParsedArtifactCandidate | null,
    reason: string,
  ) => Promise<BooleanResult | null>;
  getAutoReviewReason: (capture: CaptureResult, parsed: ParsedArtifactCandidate) => string;
  shouldFlagArtifactForReview: (parsed: ParsedArtifactCandidate | null) => boolean;
  appendAutomationLog: (line: string) => void;
  appendClickDiagnostics: (result: ClickResult, prefix?: string) => void;
  setReviewStatus: (value: string) => void;
  setAutoScanStats: (stats: AutoScanStats) => void;
  shouldStop: () => boolean;
};

export type AutoScanLoopOptions = {
  scanLimit: number;
  skipRows: number;
  detectedInventoryCount?: number | null;
  processInitialSelection?: boolean;
  skipInitialGridTarget?: boolean;
};

export type AutoScanLoopResult = {
  status: ScanSummary["status"];
  stats: AutoScanStats;
  blockedReason: string;
  pageCount: number;
  gridLabel: string;
  targetCount: number;
};

// Card-ready gating replaces a fixed settle delay: poll the detail fingerprint
// until it has changed, then read the artifact immediately. See cardReadyGate.
const CARD_READY_MAX_MS = 180;
const CARD_READY_POLL_MS = 25;
const CARD_READY_STABLE_SAMPLES = 1;
const CARD_READY_ACCEPT_CHANGED_AFTER_MS = 0;
const SCROLL_READY_MAX_MS = 760;
const SCROLL_READY_POLL_MS = 80;
const SCROLL_READY_STABLE_SAMPLES = 2;
const SCROLL_READY_ACCEPT_CHANGED_AFTER_MS = 100;
const STATS_PUBLISH_INTERVAL_MS = 250;
const ROUTINE_CLICK_LOG_INTERVAL = 12;
const MISS_ABORT_THRESHOLD = 3;
const UNREADABLE_ABORT_THRESHOLD = 5;

export async function runAutoScanLoop(
  deps: AutoScanLoopDependencies,
  options: AutoScanLoopOptions,
): Promise<AutoScanLoopResult> {
  const {
    api,
    captureSelectedSource,
    captureFastSelectedSource,
    parseArtifact,
    persistParsedArtifact,
    persistParsedArtifactsBatch,
    saveReviewSample,
    getAutoReviewReason,
    shouldFlagArtifactForReview,
    appendAutomationLog,
    appendClickDiagnostics,
    setReviewStatus,
    setAutoScanStats,
    shouldStop,
  } = deps;

  const stats: AutoScanStats = { ...emptyAutoScanStats };
  const startedAt = Date.now();
  const maxTargets = resolveScanTargetCount(options.scanLimit, options.detectedInventoryCount);
  const rowsToSkip = clampSkipRows(options.skipRows);
  const seen = new Set<string>();
  const seenPageFingerprints = new Set<string>();
  let page = 0;
  let blockedReason = "";
  let aborted = false;
  let consecutiveMisses = 0;
  let rowsQueued = 0;
  let flushingWrites = false;
  const writeQueue: Array<{ label: string; task: () => Promise<void> }> = [];
  const batchedPersistQueue: Array<{
    capture: CaptureResult | null;
    parsed: ParsedArtifactCandidate;
    source: string;
    needsReview: boolean;
  }> = [];
  let lastStatsPublishAt = 0;
  function updateStats(preserveActiveScanMs = false, forcePublish = false) {
    updateScanTiming(stats, startedAt, Date.now(), { preserveActiveScanMs: preserveActiveScanMs || flushingWrites });
    const now = Date.now();
    if (forcePublish || now - lastStatsPublishAt >= STATS_PUBLISH_INTERVAL_MS) {
      lastStatsPublishAt = now;
      setAutoScanStats({ ...stats });
    }
  }

  function enqueueWrite(label: string, task: () => Promise<void>) {
    writeQueue.push({ label, task });
  }

  async function flushWrites() {
    flushingWrites = true;
    try {
      if (persistParsedArtifactsBatch && batchedPersistQueue.length > 0) {
        const items = batchedPersistQueue.splice(0);
        try {
          stats.stored += await persistParsedArtifactsBatch(items);
        } catch (error) {
          appendAutomationLog(`write failed persist-batch:${items.length}: ${error instanceof Error ? error.message : String(error)}`);
        }
      }
      for (const item of writeQueue.splice(0)) {
        try {
          await item.task();
        } catch (error) {
          appendAutomationLog(`write failed ${item.label}: ${error instanceof Error ? error.message : String(error)}`);
        }
      }
    } finally {
      flushingWrites = false;
    }
    updateStats(true, true);
  }

  async function finish(result: AutoScanLoopResult) {
    const flushStartedAt = Date.now();
    stats.activeScanMs = Math.max(0, flushStartedAt - startedAt);
    await flushWrites();
    stats.writeFlushMs += Math.max(0, Date.now() - flushStartedAt);
    updateStats(true, true);
    return { ...result, stats: { ...stats } };
  }

  function saveAutomaticReviewSample(capture: CaptureResult | null, parsed: ParsedArtifactCandidate | null, reason: string) {
    enqueueWrite(`review:${reason}`, async () => {
      const saved = await saveReviewSample(capture, parsed, reason);
      if (saved?.ok) {
        stats.review++;
        updateStats();
      }
    });
  }

  function persistArtifactLater(capture: CaptureResult | null, parsed: ParsedArtifactCandidate, source: string, needsReview: boolean) {
    if (persistParsedArtifactsBatch) {
      batchedPersistQueue.push({ capture, parsed, source, needsReview });
      return;
    }
    enqueueWrite(`persist:${source}:${parsed.name}`, async () => {
      if (await persistParsedArtifact(capture, parsed, source, needsReview)) {
        stats.stored++;
        updateStats();
      }
    });
  }

  async function checkGuard() {
    if (shouldStop()) return "Stop-Button gedrueckt.";
    if (!api.getAutomationGuard) return "";
    try {
      const guard = await api.getAutomationGuard();
      if (guard.escapePressed) return "ESC wird gehalten - Scan sofort gestoppt.";
      if (guard.enterPressed) return "ENTER wird gehalten - Scan sofort gestoppt.";
      if (guard.f9Pressed) return "F9 wird gehalten - Scan sofort gestoppt.";
      return "";
    } catch {
      return "";
    }
  }

  function inputStopReason(result: ClickResult) {
    if (result.escapePressed) return "ESC wird gehalten - Scan sofort gestoppt.";
    if (result.enterPressed) return "ENTER wird gehalten - Scan sofort gestoppt.";
    if (result.f9Pressed) return "F9 wird gehalten - Scan sofort gestoppt.";
    return "";
  }

  async function waitDuringScan(ms: number) {
    const startedAt = Date.now();
    while (Date.now() - startedAt < ms) {
      const guardReason = await checkGuard();
      if (guardReason) return guardReason;
      await wait(Math.min(120, ms - (Date.now() - startedAt)));
    }
    return "";
  }

  async function clickTarget(target: GridTarget, label: string) {
    const clickNumber = stats.clicked + 1;
    const routineLog = clickNumber <= 2 || clickNumber % ROUTINE_CLICK_LOG_INTERVAL === 0 || label !== "click";
    if (routineLog) appendAutomationLog(`${label} r${target.row} c${target.col} -> ${target.x},${target.y}`);
    const clickStartedAt = Date.now();
    const clickResult = await api.clickScreen(target.x, target.y);
    stats.clickMs += Math.max(0, Date.now() - clickStartedAt);
    if (routineLog || reportedClickDeliveryFailure(clickResult) || clickResult.inputBlocked) {
      appendClickDiagnostics(clickResult, `${label} r${target.row} c${target.col}`);
    }
    stats.clicked++;
    stats.attempted = stats.clicked;
    updateStats();
    return clickResult;
  }

  function reportedClickDeliveryFailure(result: ClickResult) {
    return result.moved === false || result.clicked === false;
  }

  function clickDeliveryFailureReason(target: GridTarget, clickResult: ClickResult) {
    if (clickResult.inputBlocked) {
      return "Windows blockiert die Eingabe (UIPI). Starte die App als Administrator (Scanner Diagnose > 'App als Administrator neu starten').";
    }
    if (clickResult.isElevated === false) {
      return `Klick kam nicht an (Ziel ${target.x},${target.y}). Starte die App als Administrator und versuche es erneut.`;
    }
    const cursor = `${clickResult.cursorX ?? "?"},${clickResult.cursorY ?? "?"}`;
    return `Cursor kam nicht am Klick-Ziel ${target.x},${target.y} an (Cursor ${cursor}). Genshin im Vordergrund lassen und erneut versuchen.`;
  }

  let currentCapture = await captureFastSelectedSource(0, false, {
    omitFullFrame: true,
    omitDetailPreview: true,
    omitInventoryPreview: true,
    omitCrops: true,
    omitCropImages: true,
    omitLockState: true,
  });
  const initialSurfaceRejection = validateAutoScanEntryPreflight(currentCapture);
  const initialCaptureRejection = initialSurfaceRejection.ok ? captureSourceRejectionReason(currentCapture) : initialSurfaceRejection.reason;
  let gridModel = buildGridModel(currentCapture?.inventoryGrid);

  if (initialCaptureRejection || !gridModel || gridModel.targets.length === 0) {
    const reason = initialCaptureRejection || "Kein verlaessliches Kachel-Grid erkannt. Artifact-Inventar sichtbar lassen und Smart Capture einmal ausfuehren.";
    setReviewStatus(reason);
    return finish({ status: "blocked", stats, blockedReason: reason, pageCount: 0, gridLabel: reason, targetCount: maxTargets });
  }

  let lastDetailSignature = "";
  let lastDetailViewFingerprint = detailFingerprint(currentCapture);

  const shouldSkipInitialGridTarget = Boolean(options.processInitialSelection && options.skipInitialGridTarget);
  let initialProcessedOffset = 0;
  let initialSelectionDuplicateSkipped = false;
  if (options.processInitialSelection) {
    const initialCapture = await captureSelectedSource(0, false, {
      ocrMode: "artifact",
      ocrProfile: "fast",
      omitFullFrame: true,
      omitDetailPreview: true,
      omitInventoryPreview: true,
      omitCropImages: true,
      skipOcrUnlessArtifactDetail: true,
    });
    const initialSurfaceRejection = validateAutoScanEntryPreflight(initialCapture);
    if (!initialSurfaceRejection.ok) {
      return finish({
        status: "blocked",
        stats,
        blockedReason: initialSurfaceRejection.reason,
        pageCount: 0,
        gridLabel: initialSurfaceRejection.reason,
        targetCount: maxTargets,
      });
    }
    if (!initialCapture) {
      return finish({
        status: "blocked",
        stats,
        blockedReason: "Keine Capture-Daten fuer die initiale Artifact-Auswahl.",
        pageCount: 0,
        gridLabel: "Keine Capture-Daten fuer die initiale Artifact-Auswahl.",
        targetCount: maxTargets,
      });
    }

    const parsed = parseArtifact(initialCapture);
    const rejection = captureRejectionReason(initialCapture, parsed);
    if (rejection || !parsed) {
      saveAutomaticReviewSample(initialCapture, parsed, `automatic:initial-selection-rejected`);
      stats.verified++;
      stats.misses++;
      addCaptureTiming(stats, initialCapture.timings, initialCapture.elapsedMs);
      initialProcessedOffset = shouldSkipInitialGridTarget ? 1 : 0;
      lastDetailViewFingerprint = detailFingerprint(initialCapture);
      updateStats();
      appendAutomationLog(`initial selection review: ${rejection || "kein Artifact lesbar"}; scan continues with next tile`);
    } else {
      stats.verified++;
      stats.parsed++;
      addCaptureTiming(stats, initialCapture.timings, initialCapture.elapsedMs);
      initialProcessedOffset = shouldSkipInitialGridTarget ? 1 : 0;
      const signature = sessionSignature(parsed);
      seen.add(signature);
      lastDetailSignature = signature;
      lastDetailViewFingerprint = detailFingerprint(initialCapture);
      const reason = getAutoReviewReason(initialCapture, parsed);
      const needsReview = reason ? true : shouldFlagArtifactForReview(parsed);
      if (reason) saveAutomaticReviewSample(initialCapture, parsed, `automatic:${reason}:initial-selection`);
      persistArtifactLater(initialCapture, parsed, "auto-scan-initial", needsReview);
      updateStats();
      appendAutomationLog(`initial selection parsed: ${parsed.name}`);
      if (stats.parsed >= maxTargets) {
        return finish({
          status: "done",
          stats,
          blockedReason: "",
          pageCount: 1,
          gridLabel: `Initial ausgewaehltes Artifact verarbeitet, Ziel ${maxTargets} Artifacts`,
          targetCount: maxTargets,
        });
      }
    }
  }

  async function awaitCardReady() {
    const startedAt = Date.now();
    const result = await waitForCardReady(
      {
        sampleFingerprint: async () => detailFingerprint(await captureFastSelectedSource(0, false, {
          omitFullFrame: true,
          omitDetailPreview: true,
          omitInventoryPreview: true,
          omitCrops: true,
          omitCropImages: true,
          omitLockState: true,
        })),
        wait,
        now: () => Date.now(),
        checkAbort: checkGuard,
      },
      lastDetailViewFingerprint,
      {
        minStableSamples: CARD_READY_STABLE_SAMPLES,
        maxWaitMs: CARD_READY_MAX_MS,
        pollIntervalMs: CARD_READY_POLL_MS,
        acceptChangedAfterMs: CARD_READY_ACCEPT_CHANGED_AFTER_MS,
      },
    );
    addCardReadyTiming(stats, Date.now() - startedAt);
    return result;
  }

  async function awaitInventoryPageReady(previousFingerprint: string): Promise<{
    ready: Awaited<ReturnType<typeof waitForCardReady>>;
    capture: CaptureResult | null;
  }> {
    let latestCapture: CaptureResult | null = null;
    const startedAt = Date.now();
    const ready = await waitForCardReady(
      {
        sampleFingerprint: async () => {
          latestCapture = await captureFastSelectedSource(0, false, {
            omitFullFrame: true,
            omitDetailPreview: true,
            omitInventoryPreview: true,
            omitCrops: true,
            omitCropImages: true,
            omitLockState: true,
          });
          return screenFingerprint(latestCapture);
        },
        wait,
        now: () => Date.now(),
        checkAbort: checkGuard,
      },
      previousFingerprint,
      {
        minStableSamples: SCROLL_READY_STABLE_SAMPLES,
        maxWaitMs: SCROLL_READY_MAX_MS,
        pollIntervalMs: SCROLL_READY_POLL_MS,
        acceptChangedAfterMs: SCROLL_READY_ACCEPT_CHANGED_AFTER_MS,
      },
    );
    addScrollReadyTiming(stats, Date.now() - startedAt);
    return { ready, capture: latestCapture };
  }

  async function captureArtifactAfterClick(): Promise<{ capture: CaptureResult | null; fingerprint: string; abortReason: string }> {
    const capture = await captureSelectedSource(0, false, {
      ocrMode: "artifact",
      ocrProfile: "fast",
      omitFullFrame: true,
      omitDetailPreview: true,
      omitInventoryPreview: true,
      omitCropImages: true,
      skipOcrUnlessArtifactDetail: true,
    });
    return { capture, fingerprint: detailFingerprint(capture), abortReason: "" };
  }

  function validateHotArtifactCapture(capture: CaptureResult | null) {
    const sourceRejection = captureSourceRejectionReason(capture);
    if (sourceRejection) return { ok: false, reason: sourceRejection };
    if (!capture?.artifactDetail?.present) {
      const confidence = capture?.artifactDetail ? ` (${capture.artifactDetail.confidence}% Detail-Marker)` : "";
      return { ok: false, reason: `Keine Artifact-Detailansicht erkannt${confidence}. Artifact-Inventar mit sichtbarer Detailkarte offen lassen.` };
    }
    return { ok: true, reason: "" };
  }

  try {
    while (!blockedReason && !shouldStop() && stats.parsed < maxTargets) {
      page++;
      stats.pages = page;
      updateStats();

      const currentPageFingerprint = screenFingerprint(currentCapture);
      if (isRepeatedProcessedPageFingerprint(currentPageFingerprint, seenPageFingerprints, page)) {
        blockedReason = `Inventarseite ${page} wurde bereits zuvor gesehen. Scrollen hat wahrscheinlich keine neue Seite geliefert; Scan gestoppt, um keine Duplikat-Schleife zu erzeugen.`;
        break;
      }
      if (currentPageFingerprint) seenPageFingerprints.add(currentPageFingerprint);

      const pageSkipRows = page === 1 ? Math.min(rowsToSkip, Math.max(0, gridModel.rows - 1)) : 0;
      const baseTargets = gridModel.targets.filter((target) => target.row >= pageSkipRows);
      const pagePlan = buildInventoryPagePlan({
        targets: baseTargets,
        cols: gridModel.cols,
        rows: Math.max(1, gridModel.rows - pageSkipRows),
        totalTargetCount: maxTargets,
        processedTargets: stats.clicked + initialProcessedOffset,
        rowsQueued,
      });
      const targets = shouldSkipInitialGridTarget && page === 1
        ? baseTargets.slice(initialProcessedOffset)
        : pagePlan.pageTargets;

      if (targets.length === 0) {
        blockedReason = `Keine Klick-Ziele nach dem Skippen von ${pageSkipRows} Zeile(n) auf Seite ${page}.`;
        break;
      }

      setReviewStatus(`Automatischer Scan Seite ${page}: ${gridModel.cols} x ${gridModel.rows} Raster (${gridModel.source}, ${gridModel.confidence}%), ${targets.length} Klick-Ziele, ${stats.parsed}/${maxTargets} gelesen.`);
      let newArtifactsOnPage = 0;

      for (const target of targets) {
        if (shouldStop() || stats.parsed >= maxTargets) break;

        const guardReason = await checkGuard();
        if (guardReason) {
          blockedReason = guardReason;
          aborted = true;
          break;
        }

        let clickResult = await clickTarget(target, "click");
        let stopReason = inputStopReason(clickResult);
        if (stopReason) {
          blockedReason = stopReason;
          aborted = true;
          break;
        }

        if (clickResult.inputBlocked) {
          blockedReason = clickDeliveryFailureReason(target, clickResult);
          break;
        }
        if (reportedClickDeliveryFailure(clickResult)) {
          appendAutomationLog(`warn r${target.row} c${target.col}: helper reported cursor/click miss; verifying detail change`);
        }

        let read = await captureArtifactAfterClick();
        if (read.abortReason) {
          blockedReason = read.abortReason;
          aborted = true;
          break;
        }
        let capture = read.capture;
        let currentDetailFingerprint = read.fingerprint;
        let changedDetail = Boolean(currentDetailFingerprint) && currentDetailFingerprint !== lastDetailViewFingerprint;

        if (!changedDetail) {
          if (options.processInitialSelection && !initialSelectionDuplicateSkipped && !reportedClickDeliveryFailure(clickResult)) {
            initialSelectionDuplicateSkipped = true;
            consecutiveMisses = 0;
            stats.duplicates++;
            updateStats();
            appendAutomationLog(`duplicate selected tile r${target.row} c${target.col}: already processed initial detail`);
            continue;
          }
          appendAutomationLog(`retry r${target.row} c${target.col}: Detailansicht unveraendert`);
          clickResult = await clickTarget(target, "retry");
          stopReason = inputStopReason(clickResult);
          if (stopReason) {
            blockedReason = stopReason;
            aborted = true;
            break;
          }
          if (clickResult.inputBlocked) {
            blockedReason = clickDeliveryFailureReason(target, clickResult);
            break;
          }
          if (reportedClickDeliveryFailure(clickResult)) {
            appendAutomationLog(`warn r${target.row} c${target.col}: retry helper reported cursor/click miss; verifying detail change`);
          }
          read = await captureArtifactAfterClick();
          if (read.abortReason) {
            blockedReason = read.abortReason;
            aborted = true;
            break;
          }
          capture = read.capture;
          currentDetailFingerprint = read.fingerprint;
          changedDetail = Boolean(currentDetailFingerprint) && currentDetailFingerprint !== lastDetailViewFingerprint;
        }

        if (!changedDetail) {
          if (reportedClickDeliveryFailure(clickResult)) {
            blockedReason = clickDeliveryFailureReason(target, clickResult);
            break;
          }
          if (options.processInitialSelection && !initialSelectionDuplicateSkipped) {
            initialSelectionDuplicateSkipped = true;
            consecutiveMisses = 0;
            stats.duplicates++;
            updateStats();
            appendAutomationLog(`duplicate selected tile r${target.row} c${target.col}: already processed initial detail`);
            continue;
          }
          stats.misses++;
          consecutiveMisses++;
          updateStats();
          appendAutomationLog(`miss r${target.row} c${target.col}: Detailansicht unveraendert`);
          if (shouldAbortAfterConsecutiveMisses(consecutiveMisses, MISS_ABORT_THRESHOLD)) {
            blockedReason = "Mehrere Klicks hintereinander haben die Detailansicht nicht veraendert. Auto-Scan gestoppt: Klicks landen wahrscheinlich nicht auf neuen Artifacts.";
            break;
          }
          continue;
        }

        stats.verified++;

        let captureSurfaceRejection = validateHotArtifactCapture(capture);
        for (let retry = 1; !captureSurfaceRejection.ok && retry <= 2; retry++) {
          appendAutomationLog(`retry capture r${target.row} c${target.col}: ${captureSurfaceRejection.reason}`);
          await wait(120);
          read = await captureArtifactAfterClick();
          if (read.abortReason) {
            blockedReason = read.abortReason;
            aborted = true;
            break;
          }
          capture = read.capture;
          currentDetailFingerprint = read.fingerprint;
          captureSurfaceRejection = validateHotArtifactCapture(capture);
        }
        if (aborted) break;
        if (!captureSurfaceRejection.ok) {
          blockedReason = captureSurfaceRejection.reason;
          appendAutomationLog(`blocked r${target.row} c${target.col}: ${blockedReason}`);
          break;
        }
        if (capture?.ocrTimedOut) {
          addCaptureTiming(stats, capture.timings, capture.elapsedMs);
          stats.misses++;
          consecutiveMisses++;
          lastDetailViewFingerprint = detailFingerprint(capture);
          updateStats();
          appendAutomationLog(`miss r${target.row} c${target.col}: OCR timeout`);
          if (shouldAbortAfterConsecutiveMisses(consecutiveMisses, UNREADABLE_ABORT_THRESHOLD)) {
            blockedReason = "Mehrere OCR-Timeouts hintereinander. Auto-Scan gestoppt, damit die Session nicht haengen bleibt.";
            break;
          }
          continue;
        }

        const parseStartedAt = Date.now();
        const parsed = parseArtifact(capture);
        stats.parseMs += Math.max(0, Date.now() - parseStartedAt);
        const rejection = captureRejectionReason(capture, parsed);
        addCaptureTiming(stats, capture?.timings, capture?.elapsedMs);

        if (rejection) {
          saveAutomaticReviewSample(capture, parsed, `automatic:capture-rejected:p${page}:r${target.row}c${target.col}`);
          if (parsed && shouldPersistParsedArtifact(parsed, true)) {
            consecutiveMisses = 0;
            const signature = sessionSignature(parsed);
            stats.parsed++;
            lastDetailSignature = signature;
            lastDetailViewFingerprint = detailFingerprint(capture);
            seen.add(signature);
            newArtifactsOnPage++;
            persistArtifactLater(capture, parsed, "auto-scan-review", true);
            updateStats();
            continue;
          }
          stats.misses++;
          consecutiveMisses++;
          updateStats();
          appendAutomationLog(`miss r${target.row} c${target.col}: ${rejection}`);
          if (shouldAbortAfterConsecutiveMisses(consecutiveMisses, UNREADABLE_ABORT_THRESHOLD)) {
            blockedReason = "Mehrere unlesbare Artifact-Captures hintereinander. Auto-Scan gestoppt, damit nicht blind weitergeklickt wird.";
            break;
          }
          continue;
        }

        const signature = parsed ? sessionSignature(parsed) : "";
        const decision = classifyAutoScanCapture({ signature, lastDetailSignature, seen });

        if (!capture || !parsed || decision.kind === "unreadable") {
          stats.misses++;
          consecutiveMisses++;
          updateStats();
          appendAutomationLog(`miss r${target.row} c${target.col}: kein Artifact lesbar`);
          if (shouldAbortAfterConsecutiveMisses(consecutiveMisses, UNREADABLE_ABORT_THRESHOLD)) {
            blockedReason = "Mehrere unlesbare Artifacts hintereinander. Auto-Scan gestoppt: Klicks treffen wahrscheinlich nicht das Artifact-Raster.";
            break;
          }
          continue;
        }

        if (decision.kind === "stuck") {
          stats.misses++;
          consecutiveMisses++;
          updateStats();
          appendAutomationLog(`miss r${target.row} c${target.col}: Detail zeigt weiterhin "${parsed.name}"`);
          if (shouldAbortAfterConsecutiveMisses(consecutiveMisses, MISS_ABORT_THRESHOLD)) {
            blockedReason = `Mehrere Klicks blieben auf "${parsed.name}". Auto-Scan gestoppt.`;
            break;
          }
          continue;
        }

        consecutiveMisses = 0;
        stats.parsed++;
        lastDetailSignature = signature;
        lastDetailViewFingerprint = detailFingerprint(capture);

        if (decision.kind === "duplicate") {
          stats.duplicates++;
          updateStats();
          continue;
        }

        seen.add(signature);
        newArtifactsOnPage++;

        const reason = getAutoReviewReason(capture, parsed);
        const needsReview = reason ? true : shouldFlagArtifactForReview(parsed);
        if (reason) {
          saveAutomaticReviewSample(capture, parsed, `automatic:${reason}:p${page}:r${target.row}c${target.col}`);
        }
        persistArtifactLater(capture, parsed, "auto-scan", needsReview);
        updateStats();
      }

      const endOfPagePlan = buildInventoryPagePlan({
        targets: gridModel.targets.filter((target) => target.row >= pageSkipRows),
        cols: gridModel.cols,
        rows: Math.max(1, gridModel.rows - pageSkipRows),
        totalTargetCount: maxTargets,
        processedTargets: stats.clicked + initialProcessedOffset,
        rowsQueued,
      });
      rowsQueued = endOfPagePlan.rowsQueuedAfterPage;

      if (aborted || stats.parsed >= maxTargets || shouldStop() || blockedReason) break;

      if (newArtifactsOnPage === 0 && page > 1) {
        blockedReason = `Seite ${page} hat keine neuen Artifacts geliefert; gestoppt, um nicht dieselbe Seite zu loopen.`;
        break;
      }

      const guardReason = await checkGuard();
      if (guardReason) {
        blockedReason = guardReason;
        aborted = true;
        break;
      }

      const rowsToScroll = endOfPagePlan.scrollRowsAfterPage;
      if (rowsToScroll <= 0) break;
      const scrollNotches = Math.min(60, Math.max(1, rowsToScroll * 10 - 1));
      setReviewStatus(`Automatischer Scan Seite ${page} fertig. Scrolle zur naechsten Inventory-Seite...`);
      appendAutomationLog(`scroll ${scrollNotches} (${rowsToScroll} row(s)) @ ${gridModel.anchorX},${gridModel.anchorY}`);
      const scrollResult = await api.scrollScreen(-scrollNotches, gridModel.anchorX, gridModel.anchorY);
      if (scrollResult.inputBlocked) {
        blockedReason = "Scroll-Input wurde von Windows blockiert. Starte die App als Administrator.";
        break;
      }

      if (page % 12 === 0) {
        appendAutomationLog(`scroll correction p${page}: +1 notch @ ${gridModel.anchorX},${gridModel.anchorY}`);
        const correctionResult = await api.scrollScreen(1, gridModel.anchorX, gridModel.anchorY);
        if (correctionResult.inputBlocked) {
          blockedReason = "Scroll-Korrektur wurde von Windows blockiert.";
          break;
        }
      }

      const beforeScrollFingerprint = currentPageFingerprint || screenFingerprint(currentCapture);
      const scrollReady = await awaitInventoryPageReady(beforeScrollFingerprint);
      if (scrollReady.ready.abortReason) {
        blockedReason = scrollReady.ready.abortReason;
        aborted = true;
        break;
      }

      const scrolledCapture = scrollReady.capture;
      if (!scrolledCapture) {
        blockedReason = "Keine Capture-Daten nach dem Scrollen.";
        break;
      }
      currentCapture = scrolledCapture;
      const scrolledSurfaceRejection = validateAutoScanEntryPreflight(scrolledCapture);
      if (!scrolledSurfaceRejection.ok) {
        blockedReason = scrolledSurfaceRejection.reason;
        break;
      }
      const afterScrollFingerprint = screenFingerprint(scrolledCapture);

      if (beforeScrollFingerprint && afterScrollFingerprint && beforeScrollFingerprint === afterScrollFingerprint) {
        blockedReason = "Scrollen hat die sichtbare Inventarseite nicht veraendert.";
        break;
      }

      if (afterScrollFingerprint && seenPageFingerprints.has(afterScrollFingerprint)) {
        blockedReason = "Scrollen hat erneut eine bereits verarbeitete Inventarseite gezeigt.";
        break;
      }

      const refreshedModel = buildGridModel(scrolledCapture.inventoryGrid);
      if (!refreshedModel) {
        blockedReason = "Kachel-Grid nach dem Scrollen verloren.";
        break;
      }
      if (refreshedModel.source === "detected" && refreshedModel.confidence >= 72) {
        gridModel = refreshedModel;
      }
    }
  } catch (error) {
    blockedReason = `Fehler waehrend des Scans: ${error instanceof Error ? error.message : String(error)}`;
    appendAutomationLog(blockedReason);
  }

  const status: ScanSummary["status"] = aborted || shouldStop() ? "stopped" : blockedReason ? "blocked" : "done";
  updateScanTiming(stats, startedAt);
  return finish({
    status,
    stats,
    blockedReason,
    pageCount: page,
    gridLabel: blockedReason || `${page} Seite(n) verarbeitet, Ziel ${maxTargets} Artifacts, ${rowsToSkip} Zeile(n) auf der ersten Seite uebersprungen`,
    targetCount: maxTargets,
  });
}

export function detailFingerprint(capture: CaptureResult | null) {
  if (!capture) return "";
  if (capture.detailFingerprint) return capture.detailFingerprint;
  if (capture.detailDataUrl) return fingerprintDataUrl(capture.detailDataUrl);
  if (capture.dataUrl) return fingerprintDataUrl(capture.dataUrl);
  return "";
}

export function screenFingerprint(capture: CaptureResult | null) {
  if (capture?.inventoryFingerprint) return capture.inventoryFingerprint;
  if (capture?.inventoryDataUrl) return fingerprintDataUrl(capture.inventoryDataUrl);
  if (capture?.dataUrl) return fingerprintDataUrl(capture.dataUrl);
  return "";
}

export function isRepeatedProcessedPageFingerprint(
  fingerprint: string,
  seenPageFingerprints: ReadonlySet<string>,
  page: number,
) {
  return page > 1 && Boolean(fingerprint) && seenPageFingerprints.has(fingerprint);
}

export function fingerprintDataUrl(dataUrl: string) {
  let hash = 2166136261;
  const stride = Math.max(1, Math.floor(dataUrl.length / 4096));
  for (let index = 0; index < dataUrl.length; index += stride) {
    hash ^= dataUrl.charCodeAt(index);
    hash = Math.imul(hash, 16777619);
  }
  return `${dataUrl.length.toString(16)}:${(hash >>> 0).toString(16)}`;
}

function wait(ms: number): Promise<void> {
  const schedule = typeof window !== "undefined" && window.setTimeout ? window.setTimeout : setTimeout;
  return new Promise((resolve) => schedule(() => resolve(), ms));
}
