/**
 * OCR provider config - stored in `system_settings` under the key
 * `ocr.config`. Each port can either have its own row (port_id = port.id)
 * or opt into the global row (port_id = null) by setting `useGlobal: true`.
 */

import { and, eq, isNull } from 'drizzle-orm';

import { toAuditJson } from '@/lib/audit';
import { db } from '@/lib/db';
import { systemSettings } from '@/lib/db/schema/system';
import { encrypt, decrypt } from '@/lib/utils/encryption';

export type OcrProvider = 'openai' | 'claude';

export const OCR_MODELS: Record<OcrProvider, string[]> = {
  openai: ['gpt-4o-mini', 'gpt-4o', 'gpt-4-turbo'],
  claude: ['claude-haiku-4-5', 'claude-sonnet-4-6', 'claude-opus-4-7'],
};

export const DEFAULT_MODEL: Record<OcrProvider, string> = {
  openai: 'gpt-4o-mini',
  claude: 'claude-haiku-4-5',
};

/** Public shape that admin UIs read - never includes the raw key. */
export interface OcrConfigPublic {
  provider: OcrProvider;
  model: string;
  /** True when an encrypted key is present. We never echo the key itself. */
  hasApiKey: boolean;
  /** Port-level rows can opt into the global config. */
  useGlobal: boolean;
  /**
   * AI receipt parsing is opt-in per port. When false (the default),
   * the scanner uses the in-browser Tesseract.js engine and the AI
   * provider is never called even if a key is configured.
   */
  aiEnabled: boolean;
  /**
   * CM-6: manual-entry mode. When true the scanner skips ALL parsing
   * (Tesseract + AI) and presents an empty form for the operator to fill in
   * by hand. Per-port; takes precedence over `aiEnabled`. Default false.
   */
  manualEntry: boolean;
}

/** Internal shape including the decrypted key - server-side only. */
export interface OcrConfigResolved extends OcrConfigPublic {
  apiKey: string | null;
  /** Source of the resolved row: 'port' | 'global' | 'none'. */
  source: 'port' | 'global' | 'none';
}

interface StoredOcrConfig {
  provider: OcrProvider;
  model: string;
  apiKeyEncrypted: string | null;
  useGlobal: boolean;
  aiEnabled?: boolean;
  manualEntry?: boolean;
}

const KEY = 'ocr.config';

async function readRow(portId: string | null): Promise<StoredOcrConfig | null> {
  const where =
    portId === null
      ? and(eq(systemSettings.key, KEY), isNull(systemSettings.portId))
      : and(eq(systemSettings.key, KEY), eq(systemSettings.portId, portId));
  const [row] = await db.select().from(systemSettings).where(where);
  if (!row) return null;
  return row.value as unknown as StoredOcrConfig;
}

async function writeRow(portId: string | null, value: StoredOcrConfig, userId: string) {
  // True upsert. The previous delete-then-insert pattern had a race
  // window where two concurrent writes could both DELETE and both INSERT,
  // accumulating duplicate rows (caught and dedupe'd by migration 0047).
  // The (key, port_id) NULLS NOT DISTINCT unique index makes this
  // upsert atomic.
  await db
    .insert(systemSettings)
    .values({
      key: KEY,
      portId,
      value: toAuditJson(value),
      updatedBy: userId,
    })
    .onConflictDoUpdate({
      target: [systemSettings.key, systemSettings.portId],
      set: {
        value: toAuditJson(value),
        updatedBy: userId,
        updatedAt: new Date(),
      },
    });
}

/**
 * Resolve the active OCR config for a port: port row (unless `useGlobal`),
 * falling back to the global row, falling back to a default-empty config.
 */
export async function getResolvedOcrConfig(portId: string): Promise<OcrConfigResolved> {
  const portRow = await readRow(portId);
  const useGlobal = portRow?.useGlobal === true || !portRow;
  const sourceRow = useGlobal ? await readRow(null) : portRow;
  if (!sourceRow) {
    return {
      provider: 'openai',
      model: DEFAULT_MODEL.openai,
      apiKey: null,
      hasApiKey: false,
      useGlobal: portRow?.useGlobal === true,
      aiEnabled: false,
      manualEntry: portRow?.manualEntry === true,
      source: 'none',
    };
  }
  // The aiEnabled / manualEntry flags are per-port: even if the port falls back
  // to a global key, the port admin still has to flip these on this port.
  const aiEnabled = portRow?.aiEnabled === true;
  const manualEntry = portRow?.manualEntry === true;
  return {
    provider: sourceRow.provider,
    model: sourceRow.model,
    apiKey: sourceRow.apiKeyEncrypted ? decrypt(sourceRow.apiKeyEncrypted) : null,
    hasApiKey: Boolean(sourceRow.apiKeyEncrypted),
    useGlobal: portRow?.useGlobal === true,
    aiEnabled,
    manualEntry,
    source: useGlobal ? 'global' : 'port',
  };
}

/** Public-safe view for the admin UI - same shape but never the key. */
export async function getPublicOcrConfig(portId: string | null): Promise<OcrConfigPublic> {
  const row = await readRow(portId);
  if (!row) {
    return {
      provider: 'openai',
      model: DEFAULT_MODEL.openai,
      hasApiKey: false,
      useGlobal: false,
      aiEnabled: false,
      manualEntry: false,
    };
  }
  return {
    provider: row.provider,
    model: row.model,
    hasApiKey: Boolean(row.apiKeyEncrypted),
    useGlobal: row.useGlobal,
    aiEnabled: row.aiEnabled === true,
    manualEntry: row.manualEntry === true,
  };
}

export interface SaveOcrConfigInput {
  provider: OcrProvider;
  model: string;
  /** When provided, replaces any stored key. When undefined, the existing key is preserved. */
  apiKey?: string;
  /** When true, clears the stored key. */
  clearApiKey?: boolean;
  useGlobal?: boolean;
  /** Per-port toggle: enable AI receipt parsing. Defaults to false. */
  aiEnabled?: boolean;
  /** Per-port toggle: manual entry (skip all parsing). Defaults to false. */
  manualEntry?: boolean;
}

export async function saveOcrConfig(
  portId: string | null,
  input: SaveOcrConfigInput,
  userId: string,
): Promise<void> {
  const existing = await readRow(portId);
  let apiKeyEncrypted = existing?.apiKeyEncrypted ?? null;
  if (input.clearApiKey) {
    apiKeyEncrypted = null;
  } else if (input.apiKey !== undefined && input.apiKey.length > 0) {
    apiKeyEncrypted = encrypt(input.apiKey);
  }
  // AI is meaningful only at the port scope. Preserve the existing flag if the
  // caller didn't pass one (so toggling provider/model doesn't re-disable AI).
  const aiEnabled = portId === null ? false : (input.aiEnabled ?? existing?.aiEnabled ?? false);
  // Manual entry is also port-only; preserve when the caller omits it.
  const manualEntry =
    portId === null ? false : (input.manualEntry ?? existing?.manualEntry ?? false);
  await writeRow(
    portId,
    {
      provider: input.provider,
      model: input.model,
      apiKeyEncrypted,
      useGlobal: portId === null ? false : Boolean(input.useGlobal),
      aiEnabled,
      manualEntry,
    },
    userId,
  );
}
