/**
 * deal.ts — translateDealFields orchestrator.
 *
 * Core translation pipeline for a single deal × target-locale pair.
 * Called by the queue consumer (Task 12) after atomically claiming a job.
 *
 * Algorithm per translatable field:
 *   1. Hash source text. If hash unchanged + !forceFresh + !manualOverride → skip.
 *   2. manualOverride=true → skip unconditionally (preserve human edit).
 *   3. Split source text into sentence units.
 *   4. Batch-look up Translation Memory — hits are free.
 *      (Bypassed when forceFresh — re-translate everything.)
 *   5. Misses → single provider.translateBatch call.
 *   6. checkAndIncrementBudget BEFORE provider call → throws BudgetExhaustedError.
 *   7. reconcileBudget AFTER call (corrects estimated vs actual).
 *   8. tmInsertBatch for new translations. Always runs (even on forceFresh):
 *      ON CONFLICT DO UPDATE preserves existing translatedText, only bumps
 *      usageCount. New sentences (e.g. mutated paragraphs) are written to TM.
 *   9. reassembleUnits → final field text.
 *  10. upsertDealTranslationField — INSERT … ON CONFLICT DO UPDATE.
 *
 * Slug flow:
 *   • A stable placeholder slug (deal-<dealId[:8]>) is used as the INSERT
 *     seed so the row can be created before title is known.
 *   • After the field loop, ensureSlugPinned overwrites the placeholder with
 *     the translated title slug (immutable once set to a non-placeholder value).
 *
 * Re-exports BudgetExhaustedError as the single import point for consumers.
 */

import type { DrizzleClient } from '@/server/db/client.js';
import { recomputeSearchTsvector } from '@/server/translation/search.js';
import type { DealTranslation } from '@/server/db/schema.js';
import { hashUnit } from '@/server/translation/memory/normalize.js';
import { splitIntoUnits, reassembleUnits } from '@/server/translation/memory/units.js';
import { tmLookupBatch, tmInsertBatch, type TmHit } from '@/server/translation/memory/queries.js';
import { getProvider } from '@/server/translation/provider/index.js';
import {
  checkAndIncrementBudget,
  reconcileBudget,
  BudgetExhaustedError,
} from '@/server/translation/budget.js';
import {
  loadDealWithSource,
  loadDealTranslation,
  upsertDealTranslationField,
  ensureSlugPinned,
  estimateCost,
  slugify,
} from './_helpers.js';

// Re-export so queue consumer (Task 12) imports from a single place.
export { BudgetExhaustedError };

// ─── Public types ─────────────────────────────────────────────────────────────

export interface TranslateDealFieldsResult {
  inputTokens: number;
  outputTokens: number;
  costUsd: number;
  tmHits: number;
  tmMisses: number;
}

export interface TranslateDealFieldsOpts {
  dealId: string;
  targetLocale: string;
  /**
   * When true: bypass TM lookup and per-field hash dedup, forcing a fresh
   * provider call for all fields. Also skips TM insert to avoid contaminating
   * the TM with potentially bad forced translations.
   */
  forceFresh?: boolean;
}

// ─── Internal constants ───────────────────────────────────────────────────────

const TRANSLATABLE_FIELDS = ['title', 'description', 'specialInstructions'] as const;
type TranslatableField = (typeof TRANSLATABLE_FIELDS)[number];

// deal_translations column names for per-field source hashes.
const HASH_COL: Record<TranslatableField, keyof DealTranslation> = {
  title: 'titleSourceHash',
  description: 'descriptionSourceHash',
  specialInstructions: 'specialInstructionsSourceHash',
};

// deal_translations column names for per-field manual-override flags.
const OVERRIDE_COL: Record<TranslatableField, keyof DealTranslation> = {
  title: 'titleManualOverride',
  description: 'descriptionManualOverride',
  specialInstructions: 'specialInstructionsManualOverride',
};

function zeroCost(): TranslateDealFieldsResult {
  return { inputTokens: 0, outputTokens: 0, costUsd: 0, tmHits: 0, tmMisses: 0 };
}

// ─── Orchestrator ─────────────────────────────────────────────────────────────

/**
 * Translate all translatable fields of a deal into `targetLocale`.
 *
 * @throws BudgetExhaustedError  when the daily budget is exhausted; the caller
 *   (queue consumer, Task 12) should catch this and mark the job PENDING_BUDGET.
 */
export async function translateDealFields(
  db: DrizzleClient,
  opts: TranslateDealFieldsOpts,
): Promise<TranslateDealFieldsResult> {
  const { dealId, targetLocale, forceFresh = false } = opts;

  const deal = await loadDealWithSource(db, dealId);

  // Short-circuit: nothing to do when source locale equals target locale.
  if (deal.sourceLanguage === targetLocale) return zeroCost();

  // Load the existing translation row once (used for hash dedup + override checks).
  const existing = await loadDealTranslation(db, dealId, targetLocale);

  const provider = await getProvider(db);

  let totalIn = 0;
  let totalOut = 0;
  let totalCost = 0;
  let hits = 0;
  let misses = 0;

  // Track the translated title for slug generation (set after title field is processed).
  let translatedTitle: string | null = null;

  // Stable placeholder slug used as the seed for upsertDealTranslationField INSERT.
  // ensureSlugPinned will replace it with the translated-title slug after the loop.
  // If the row already exists its slug is already set — this placeholder is never used.
  const placeholderSlug = `deal-${dealId.slice(0, 8)}`;

  // ── Field loop ──────────────────────────────────────────────────────────────

  for (const field of TRANSLATABLE_FIELDS) {
    const sourceText = ((deal as Record<string, unknown>)[field] as string | null) ?? '';

    // Skip empty source fields (specialInstructions is often null).
    if (!sourceText) continue;

    // ── Skip: manualOverride ─────────────────────────────────────────────────
    // Human-edited value — preserve unconditionally, even with forceFresh.
    if (existing?.[OVERRIDE_COL[field]]) continue;

    // ── Skip: hash dedup ─────────────────────────────────────────────────────
    // If source text is unchanged since last translation, skip (free dedup).
    const newHash = await hashUnit(sourceText, deal.sourceLanguage);

    if (!forceFresh && existing && existing[HASH_COL[field]] === newHash) {
      // Carry forward existing translated title for slug generation.
      if (field === 'title' && existing.title) translatedTitle = existing.title;
      continue;
    }

    // ── Split into translation units ─────────────────────────────────────────
    const units = splitIntoUnits(sourceText, deal.sourceLanguage);

    // ── TM lookup ────────────────────────────────────────────────────────────
    // Returns Map<unitIndex, TmHit>. forceFresh bypasses (empty map = all misses).
    const tmMap = forceFresh
      ? new Map<number, TmHit>()
      : await tmLookupBatch(
          db,
          units.map((u) => ({
            text: u.text,
            srcLocale: deal.sourceLanguage,
            tgtLocale: targetLocale,
          })),
        );

    // ── Collect misses ────────────────────────────────────────────────────────
    const missIndices = units.map((_, i) => i).filter((i) => !tmMap.has(i));
    const missTexts = missIndices.map((i) => units[i]!.text);

    // ── Provider call ─────────────────────────────────────────────────────────
    let llmTranslations: string[] = [];

    if (missTexts.length > 0) {
      // Budget gate — throws BudgetExhaustedError if daily limit exceeded.
      const estCost = await estimateCost(db, missTexts);
      await checkAndIncrementBudget(db, estCost);

      // Build glossary from TM hits to guide the provider tone/terminology.
      const glossary = [...tmMap.values()]
        .slice(0, 20)
        .map((h) => ({ source: h.sourceText, translation: h.translatedText }));

      const resp = await provider.translateBatch({
        sourceLocale: deal.sourceLanguage,
        targetLocale,
        texts: missTexts,
        glossary,
        style: 'marketing',
      });

      llmTranslations = resp.translations;
      totalIn += resp.inputTokens;
      totalOut += resp.outputTokens;
      totalCost += resp.costUsd;

      // Reconcile budget: adjust running total by (actual - estimated).
      await reconcileBudget(db, estCost, resp.costUsd);

      // Insert new TM rows. ON CONFLICT DO UPDATE bumps usageCount/lastUsedAt
      // but preserves existing translatedText — safe to run even on forceFresh.
      // Skipping on forceFresh was incorrect: new sentences (e.g. mutated
      // paragraphs) must be written to TM regardless of forceFresh flag.
      const missHashes = await Promise.all(missTexts.map((t) => hashUnit(t, deal.sourceLanguage)));
      await tmInsertBatch(
        db,
        missTexts.map((t, j) => ({
          sourceHash: missHashes[j]!,
          sourceText: t,
          translatedText: llmTranslations[j]!,
          modelId: resp.modelId,
          sourceLocale: deal.sourceLanguage,
          targetLocale,
        })),
      );
    }

    // ── Reassemble ────────────────────────────────────────────────────────────
    const translatedUnits = units.map((_, i) => {
      if (tmMap.has(i)) {
        hits++;
        return tmMap.get(i)!.translatedText;
      }
      misses++;
      return llmTranslations[missIndices.indexOf(i)]!;
    });

    const reassembled = reassembleUnits(units, translatedUnits);

    // ── Persist field ─────────────────────────────────────────────────────────
    // Slug seed: use existing slug if row exists, else placeholder.
    const slugSeed = existing?.slug || placeholderSlug;
    await upsertDealTranslationField(
      db,
      dealId,
      targetLocale,
      slugSeed,
      field,
      reassembled,
      newHash,
      provider.id,
    );

    if (field === 'title') translatedTitle = reassembled;
  }

  // ── Slug pinning ─────────────────────────────────────────────────────────────
  // Generate the canonical slug from the translated title (or source title if
  // title was skipped due to dedup/override). ensureSlugPinned is a no-op when
  // the row already has a non-empty slug (immutable once set).
  //
  // Note: if NO fields were translated (all skipped), the row may not exist yet.
  // In that case ensureSlugPinned creates the slug-only row on first call.
  const titleForSlug = translatedTitle ?? deal.title;
  const candidateSlug = slugify(titleForSlug, dealId);
  await ensureSlugPinned(db, dealId, targetLocale, candidateSlug);

  // ── Search tsvector ──────────────────────────────────────────────────────────
  // Populate deal_translations.search with a weighted tsvector (A=title,
  // B=description, C=specialInstructions) using the per-locale regconfig.
  await recomputeSearchTsvector(db, dealId, targetLocale);

  return {
    inputTokens: totalIn,
    outputTokens: totalOut,
    costUsd: totalCost,
    tmHits: hits,
    tmMisses: misses,
  };
}
