/** Default margin (px) around tracked bounds when cropping a card capture. */
export const CAPTURE_MARGIN_PX = 20;

/** Vision rate-limit backoff duration (ms). */
export const VISION_RATE_LIMIT_MS = 60_000;

export function getScanAuthHeaders() {
  return {
    'Content-Type': 'application/json',
    Authorization: `Bearer ${localStorage.getItem('auth_token')}`,
  };
}

export function candidateMatchesSetHint(candidate, setName, setCode) {
  if (!setName && !setCode) return true;
  const hint = (setName || setCode || '').toLowerCase();
  const setNameLower = (candidate.set_name || '').toLowerCase();
  const setCodeLower = (candidate.set_code || '').toLowerCase();
  return setNameLower.includes(hint) || hint.includes(setNameLower) || setCodeLower === hint;
}

export function buildOcrMetaFromIdentifyResult(result) {
  return {
    confidence: result.ocr?.confidence ?? result.card?.ocr?.confidence,
    rawText: result.ocr?.rawText ?? result.card?.ocr?.rawText,
    abilities: result.card?.ocr?.abilities || [],
    hp: result.card?.hp,
    manaCost: result.card?.mana_cost,
    cardName: result.ocr?.cardName,
    query: result.ocr?.query,
  };
}

export function buildScannedCardPayload(finalCard, { imageData, scanImageUrl, ocrMeta = {} }) {
  return {
    name: finalCard.name,
    set: finalCard.set_name,
    setCode: finalCard.set_code,
    cardNumber: finalCard.card_number,
    game: finalCard.game,
    cardType: finalCard.card_type,
    rarity: finalCard.rarity,
    hp: finalCard.hp || ocrMeta.hp,
    manaCost: finalCard.mana_cost || ocrMeta.manaCost,
    abilities: ocrMeta.abilities || [],
    ocrText: ocrMeta.rawText,
    confidence: ocrMeta.confidence,
    capturedImage: imageData,
    scanImageUrl,
    image_url: finalCard.image_url,
    databaseId: finalCard.id,
    isExisting: true,
  };
}

/**
 * Map a server identify response to a UI action the scanner component applies.
 * @returns {{ type: string, cardStatus: string, [key: string]: unknown }}
 */
export function resolveIdentifyOutcome(result) {
  if (!result.isCard) {
    return {
      type: 'error',
      cardStatus: 'negative',
      message: result.reason || 'No trading card detected',
    };
  }

  const ocrMeta = buildOcrMetaFromIdentifyResult(result);

  if (result.card) {
    return { type: 'emit', cardStatus: 'confirmed', card: result.card, ocrMeta };
  }

  if (result.needsUserSelection && result.matches?.length) {
    return {
      type: 'disambiguation',
      cardStatus: 'confirmed',
      matches: result.matches,
      ocrMeta,
      message: result.message,
    };
  }

  if (result.needsReview) {
    return {
      type: 'notice',
      cardStatus: 'confirmed',
      message: result.message || 'Scan saved for admin review.',
    };
  }

  if (result.needsUserInput) {
    return {
      type: 'error',
      cardStatus: 'negative',
      message: result.message || 'Could not identify card — try again or submit for review.',
    };
  }

  return {
    type: 'error',
    cardStatus: 'negative',
    message: 'Could not identify card from scan.',
  };
}

/**
 * After a vision refine call during disambiguation, decide the next UI step.
 */
export function resolveDisambiguationRefineAction(result, { candidates }) {
  if (result.needsReview) {
    return { type: 'review_submitted', message: result.message };
  }

  if (result.card) {
    return {
      type: 'emit',
      card: result.card,
      ocrMeta: {
        confidence: result.ocr?.confidence,
        rawText: result.ocr?.rawText,
        abilities: result.card?.ocr?.abilities || [],
      },
    };
  }

  const setHint = result.ocr?.setName || result.ocr?.setCode;
  if (!result.matches?.length || !setHint) {
    return { type: 'noop' };
  }

  const filtered = candidates.filter((candidate) =>
    candidateMatchesSetHint(candidate, result.ocr.setName, result.ocr.setCode)
  );

  if (filtered.length === 0) {
    return {
      type: 'catalog_gap',
      setHint,
      submitPayload: {
        name: result.ocr?.cardName || candidates[0]?.name,
        candidateCardIds: candidates.map((c) => c.id),
      },
      hintMessage: `No "${setHint}" printing in our catalog. Tap "My card isn't listed" to save for admin review.`,
    };
  }

  if (filtered.length === 1) {
    return { type: 'pick', candidate: filtered[0] };
  }

  if (filtered.length > 1 && filtered.length < candidates.length) {
    return {
      type: 'narrow',
      filtered,
      visionHint: setHint,
      message: `Narrowed to ${filtered.length} printings matching "${setHint}".`,
    };
  }

  return { type: 'noop' };
}

/** Crop a tracked card region from the live video frame; returns a JPEG data URL. */
export function captureCardRegionFromVideo(video, canvas, bounds, margin = CAPTURE_MARGIN_PX) {
  const ctx = canvas.getContext('2d');
  const { x, y, width, height } = bounds;

  canvas.width = width + margin * 2;
  canvas.height = height + margin * 2;

  ctx.drawImage(
    video,
    Math.max(0, x - margin),
    Math.max(0, y - margin),
    width + margin * 2,
    height + margin * 2,
    0,
    0,
    canvas.width,
    canvas.height
  );

  return canvas.toDataURL('image/jpeg', 0.8);
}

export async function submitScanForReview({ imageData, name, candidateCardIds, authHeaders }) {
  const response = await fetch('/api/scan/submit-for-review', {
    method: 'POST',
    headers: authHeaders,
    body: JSON.stringify({ imageData, name, candidateCardIds }),
  });

  if (response.status === 429) {
    return { ok: false, rateLimited: true };
  }

  if (!response.ok) {
    const errBody = await response.json().catch(() => ({}));
    throw new Error(errBody.error || 'Failed to submit scan for review');
  }

  const result = await response.json();
  return { ok: true, message: result.message };
}

export async function fetchIdentifyByText({ ocrText, ocrConfidence, authHeaders }) {
  const response = await fetch('/api/cards/identify-by-text', {
    method: 'POST',
    headers: authHeaders,
    body: JSON.stringify({ ocrText, ocrConfidence }),
  });

  if (!response.ok) {
    return { ok: false };
  }

  const result = await response.json();
  return { ok: true, result };
}

export async function fetchVisionIdentify(imageData, authHeaders) {
  const response = await fetch('/api/scan/identify', {
    method: 'POST',
    headers: authHeaders,
    body: JSON.stringify({ imageData }),
  });

  if (response.status === 429) {
    return { ok: false, rateLimited: true };
  }

  if (!response.ok) {
    const errBody = await response.json().catch(() => ({}));
    throw new Error(errBody.error || `Scan identify failed: ${response.status}`);
  }

  const result = await response.json();
  return { ok: true, result };
}

/**
 * Layer 1: local OCR name strip + pg_trgm catalog match.
 * Returns { handled: true, outcome } when L1 resolves without escalation.
 */
export async function tryLayer1TextIdentify(imageData, authHeaders) {
  const { recognizeCardNameStrip } = await import('./ocr-worker.js');
  const ocr = await recognizeCardNameStrip(imageData);

  if (ocr.text.length < 3) {
    return { handled: false };
  }

  const l1 = await fetchIdentifyByText({
    ocrText: ocr.text,
    ocrConfidence: ocr.confidence,
    authHeaders,
  });

  if (!l1.ok || l1.result.escalate) {
    return { handled: false };
  }

  return {
    handled: true,
    outcome: resolveIdentifyOutcome(l1.result),
  };
}

/**
 * Run Layer 1 then Layer 2 identification for a tracked card capture.
 */
export async function identifyTrackedCardCapture({
  video,
  canvas,
  cardTracker,
  authHeaders,
  visionCooldownUntilMs = 0,
}) {
  const imageData = captureCardRegionFromVideo(video, canvas, cardTracker.bounds);

  try {
    const l1 = await tryLayer1TextIdentify(imageData, authHeaders);
    if (l1.handled) {
      return { imageData, ...l1 };
    }
  } catch (l1Error) {
    console.warn('Layer-1 OCR path failed, escalating to vision:', l1Error);
  }

  if (Date.now() < visionCooldownUntilMs) {
    return { imageData, retry: true };
  }

  const vision = await fetchVisionIdentify(imageData, authHeaders);
  if (vision.rateLimited) {
    return { imageData, rateLimited: true };
  }

  return {
    imageData,
    handled: true,
    outcome: resolveIdentifyOutcome(vision.result),
  };
}
