/**
 * Live capture cycle: one snapshot fetched through the native helper and run
 * through the processing pipeline. Shared by `test-snapshot` (which prints the
 * decision without storing) and available to a future capture scheduler.
 *
 * The routing (AX vs OCR) happens here because the native OCR call is async
 * while the processor's OCR seam is sync: a terminal-class or AX-empty window
 * has its OCR text fetched eagerly, then handed to the processor as
 * pre-extracted text. When OCR fails, the candidate is left text-less so the
 * processor skips it (ocr-unavailable) rather than storing empty AX text.
 */

import { extractAxText } from "./axtree.js";
import { isTerminalApp } from "./capture.js";
import { matchDenyRule } from "./denylist.js";
import type { CaptureCandidate, CaptureDecision, CaptureProcessor } from "./capture.js";
import type { DaemonConfig } from "./config.js";
import type { AxSnapshot, NativeHelper } from "./helper.js";

export async function captureViaHelper(
  helper: NativeHelper,
  processor: CaptureProcessor,
  config: DaemonConfig,
  capturedAtUtc: string,
): Promise<CaptureDecision> {
  const snap = await helper.axSnapshot({ frontmost: true, maxNodes: config.maxNodes });
  return captureFromSnapshot(snap, helper, processor, config, capturedAtUtc);
}

/**
 * Run an already-fetched AX snapshot through the pipeline. The live scheduler
 * uses this so a single ax-snapshot poll drives both change detection and the
 * capture, avoiding a redundant fetch.
 */
export async function captureFromSnapshot(
  snap: AxSnapshot,
  helper: NativeHelper,
  processor: CaptureProcessor,
  config: DaemonConfig,
  capturedAtUtc: string,
): Promise<CaptureDecision> {
  const axText = extractAxText(snap.tree, config.maxNodes).text;
  const candidate: CaptureCandidate = {
    capturedAtUtc,
    app: snap.app,
    windowTitle: snap.windowTitle,
    ...(snap.browserUrl != null ? { browserUrl: snap.browserUrl } : {}),
  };
  // Deny preflight: never OCR/screen-capture a deny-listed window. process()
  // applies deny too, but only AFTER text extraction — so without this an OCR
  // call would fire against a denied window before the rule is checked.
  const denied =
    matchDenyRule(
      { app: snap.app, windowTitle: snap.windowTitle, browserUrl: snap.browserUrl ?? null },
      { apps: config.denyApps, titles: config.denyTitles, urls: config.denyUrls },
    ) !== null;
  if (denied) {
    // Leave text-less; processor.process denies it below (no OCR ran).
  } else if (isTerminalApp(snap.app, config.terminalApps) || axText.trim() === "") {
    try {
      // Pin OCR to the snapshot's window when the helper reported its id: a
      // focus change between the ax-snapshot and this call must not redirect
      // OCR at a different window than the one this candidate is stored
      // under. The deny preflight above ran against this same window
      // identity, so the resolved OCR target is the window that was checked.
      // Without an id (legacy helper) frontmost is the only target available
      // and the preflight against the snap identity remains the guard.
      const ocrText = await helper.ocrWindow(
        snap.windowId !== undefined ? { windowId: snap.windowId } : { frontmost: true },
      );
      if (ocrText.trim() !== "") {
        candidate.text = ocrText;
        candidate.textSource = "ocr";
      }
      // Blank OCR: leave text-less so the processor reports ocr-unavailable
      // instead of persisting an empty snapshot.
    } catch {
      // OCR unavailable/failed: leave text-less so the processor reports
      // ocr-unavailable instead of persisting an empty snapshot.
    }
  } else {
    candidate.text = axText;
    candidate.textSource = "ax";
  }
  return processor.process(candidate);
}
