import { createDbService } from '@/server/services/db.js';
/**
 * GeminiSupportAgent — full agent loop implementation.
 *
 * Registered via getAiAgents() in registry.ts when GOOGLE_API_KEY is present.
 * Requires GOOGLE_API_KEY to construct; throws SupportAgentUnavailableError otherwise.
 */

import { requireConfiguredLlmModel } from '../../model-config.js';
import { computeCostUsd } from '../../pricing.js';
import { geminiProvider } from '../../providers/gemini.js';
import type { LlmProvider, NormalizedTextResult } from '../../providers/types.js';
import type { AiCredentials } from '../../credentials.js';
import { ProviderTransientError, ProviderRateLimitError } from '../../providers/types.js';
import type { SupportAgent, SupportAgentRunInput, SupportAgentRunResult } from '../../types.js';
import { buildSystemPrompt } from './prompt.js';
import { TOOL_SCHEMAS, TOOL_IMPLS, SUPPORT_AGENT_NAME, type ToolName } from './tools.js';
import * as interventionsQ from '@/server/db/queries/ai-interventions.js';
import * as ticketQ from '@/server/db/queries/support-tickets.js';
import * as caseQ from '@/server/db/queries/support-cases.js';
import * as configQ from '@/server/db/queries/system-config.js';
import { withSentry } from '@/server/observability/with-sentry';
import { captureCaught } from '@/server/observability/capture.server';
import { buildRedactionMap } from './redactor.js';
import type { AgentDeps, AgentRunState, SupportConfigSnapshot } from './types.js';

// ─── Terminal status sets ──────────────────────────────────────────────────────

const TICKET_TERMINAL = new Set(['resolved', 'closed']);
const CASE_TERMINAL = new Set(['resolved', 'closed', 'human_review']);

// Terminal tools that break the loop
const TERMINAL_TOOLS = new Set<ToolName>(['escalate_to_human', 'close_ticket', 'propose_refund']);

// ─── JSON parse helper ────────────────────────────────────────────────────────

interface AgentTurn {
  tool: ToolName;
  input: unknown;
  confidence: number;
  reasoning: string;
}

function parseTurn(text: string): AgentTurn | null {
  try {
    // Strip markdown code fences if present
    const cleaned = text
      .replace(/^```(?:json)?\s*/i, '')
      .replace(/\s*```\s*$/i, '')
      .trim();
    const parsed = JSON.parse(cleaned);
    if (
      typeof parsed === 'object' &&
      parsed !== null &&
      typeof parsed.tool === 'string' &&
      typeof parsed.confidence === 'number' &&
      'input' in parsed
    ) {
      return parsed as AgentTurn;
    }
    return null;
  } catch (err) {
    captureCaught(err, { scope: 'server.ai.agents.support.agent', severity: 'warning' });
    return null;
  }
}

// ─── Core loop function ────────────────────────────────────────────────────────

/**
 * Run the support agent loop with the given dependencies and input.
 *
 * This is the main entry point used by both GeminiSupportAgent and directly
 * in tests with injected deps.
 */
export const runSupportAgent = withSentry(runSupportAgentImpl, {
  name: 'ai.support.run',
  kind: 'ai-agent',
});

/**
 * Call provider with primary model; on ProviderTransientError or
 * ProviderRateLimitError, retry once with fallbackModel.
 * Returns result + the model name that succeeded.
 */
async function callWithFallback(
  provider: LlmProvider,
  creds: AiCredentials,
  prompt: string,
  model: string,
  fallbackModel: string,
  imageData?: { base64: string; mimeType: string },
): Promise<NormalizedTextResult & { usedModelName: string }> {
  async function attempt(m: string): Promise<NormalizedTextResult> {
    if (imageData) {
      // base64 → ArrayBuffer for provider interface
      const binary = atob(imageData.base64);
      const buf = new ArrayBuffer(binary.length);
      const view = new Uint8Array(buf);
      for (let i = 0; i < binary.length; i++) view[i] = binary.charCodeAt(i);
      return provider.generateTextWithImage(creds, {
        model: m,
        prompt,
        imageBytes: buf,
        mime: imageData.mimeType,
      });
    }
    return provider.generateText(creds, { model: m, prompt });
  }

  try {
    const result = await attempt(model);
    return { ...result, usedModelName: model };
  } catch (err) {
    if (err instanceof ProviderTransientError || err instanceof ProviderRateLimitError) {
      const result = await attempt(fallbackModel);
      return { ...result, usedModelName: fallbackModel };
    }
    throw err;
  }
}

async function runSupportAgentImpl(
  deps: AgentDeps,
  input: SupportAgentRunInput,
): Promise<SupportAgentRunResult> {
  const { db, provider, creds, piiKey, config, model, fallbackModel, runState } = deps;

  // ── 1. Load parent row. If terminal → noop return. ─────────────────────────
  if (input.parentType === 'ticket') {
    const ticket = await ticketQ.findById(db, input.parentId);
    if (!ticket || TICKET_TERMINAL.has(ticket.status)) {
      return toResult(runState);
    }
  } else {
    const c = await caseQ.findById(db, input.parentId);
    if (!c || CASE_TERMINAL.has(c.status)) {
      return toResult(runState);
    }
  }

  // ── 2. Build system prompt ──────────────────────────────────────────────────
  const systemPrompt = await buildSystemPrompt(deps, []);

  // ── 3. Agent loop ───────────────────────────────────────────────────────────
  let conversationContext = systemPrompt;
  let jsonFailures = 0;

  while (runState.toolCallsMade < config.maxToolCalls) {
    const loopStart = Date.now();

    // ── 3a. Call Gemini ───────────────────────────────────────────────────────
    let text: string;
    let promptTokens: number;
    let completionTokens: number;
    let usedModelName: string;

    if (runState.pendingImageData) {
      const imageData = runState.pendingImageData;
      runState.pendingImageData = null;
      const resp = await callWithFallback(
        provider,
        creds,
        conversationContext,
        model,
        fallbackModel,
        imageData,
      );
      text = resp.text;
      promptTokens = resp.usage.promptTokens;
      completionTokens = resp.usage.completionTokens;
      usedModelName = resp.usedModelName;
    } else {
      const resp = await callWithFallback(
        provider,
        creds,
        conversationContext,
        model,
        fallbackModel,
      );
      text = resp.text;
      promptTokens = resp.usage.promptTokens;
      completionTokens = resp.usage.completionTokens;
      usedModelName = resp.usedModelName;
    }

    const latencyMs = Date.now() - loopStart;

    // ── 3b. Accumulate cost ───────────────────────────────────────────────────
    const callCost = computeCostUsd(usedModelName, promptTokens, completionTokens) ?? 0;
    runState.cumulativeCostUsd += callCost;

    if (runState.cumulativeCostUsd > config.maxCostCents / 100) {
      // Cost cap exceeded → force escalation
      await forceEscalate(deps, input, 'cost_cap_exceeded', {
        promptTokens,
        completionTokens,
        usedModelName,
        latencyMs,
        callCost,
      });
      runState.decision = 'escalate';
      runState.toolCallsMade++;
      break;
    }

    // ── 3c. Parse JSON output ─────────────────────────────────────────────────
    const turn = parseTurn(text);
    if (!turn) {
      jsonFailures++;
      if (jsonFailures >= 2) {
        // Two failures → escalate
        await forceEscalate(deps, input, 'invalid_json', {
          promptTokens,
          completionTokens,
          usedModelName,
          latencyMs,
          callCost,
        });
        runState.decision = 'escalate';
        runState.toolCallsMade++;
        break;
      }
      // Re-prompt once
      conversationContext = [
        conversationContext,
        `[system]: Your response must be JSON with keys: tool, input, confidence, reasoning. Got: ${text.slice(0, 200)}`,
      ].join('\n\n');
      continue;
    }
    jsonFailures = 0; // reset on valid parse
    runState.currentConfidence = turn.confidence;

    const effectiveTool = turn.tool as ToolName;
    // ── 3d. Validate tool input ───────────────────────────────────────────────
    const schema = TOOL_SCHEMAS[effectiveTool];
    if (!schema) {
      // Unknown tool → re-prompt once
      conversationContext = [
        conversationContext,
        `[system]: Unknown tool "${effectiveTool}". Choose from: ${Object.keys(TOOL_SCHEMAS).join(', ')}`,
        `[assistant]: ${text}`,
      ].join('\n\n');
      continue;
    }

    const parsed = schema.safeParse(turn.input);
    if (!parsed.success) {
      jsonFailures++;
      if (jsonFailures >= 2) {
        await forceEscalate(deps, input, 'invalid_tool_input', {
          promptTokens,
          completionTokens,
          usedModelName,
          latencyMs,
          callCost,
        });
        runState.decision = 'escalate';
        runState.toolCallsMade++;
        break;
      }
      conversationContext = [
        conversationContext,
        `[assistant]: ${text}`,
        `[system]: Tool input validation failed: ${parsed.error.message}. Fix and retry.`,
      ].join('\n\n');
      continue;
    }
    jsonFailures = 0;

    // ── 3e. Execute tool ──────────────────────────────────────────────────────
    const toolResult = await TOOL_IMPLS[effectiveTool](deps, turn.input);
    runState.toolCallsMade++;

    // Map tool name to decision for intervention logging
    const decision = toolToDecision(effectiveTool);
    runState.decision = decision;

    // ── 3f. Write ai_interventions row ────────────────────────────────────────
    await interventionsQ.insertEncrypted(
      db,
      {
        parentType: input.parentType,
        parentId: input.parentId,
        agentName: SUPPORT_AGENT_NAME,
        toolName: effectiveTool,
        confidence: String(turn.confidence),
        decision,
        tokensIn: promptTokens,
        tokensOut: completionTokens,
        latencyMs,
        costUsd: String(callCost.toFixed(6)),
      },
      JSON.stringify(turn.input),
      JSON.stringify(toolResult),
      piiKey,
    );

    // ── 3g. If terminal tool → break ──────────────────────────────────────────
    if (TERMINAL_TOOLS.has(effectiveTool)) {
      break;
    }

    // Append result to next turn context
    const resultSummary = JSON.stringify(toolResult).slice(0, 2048);
    conversationContext = [
      conversationContext,
      `[assistant]: ${text}`,
      `[tool_result:${effectiveTool}]: ${resultSummary}`,
    ].join('\n\n');

    // If read_image returned data, store it for next Gemini call
    if (effectiveTool === 'read_image' && toolResult.ok) {
      const data = toolResult.data as { base64: string; mimeType: string };
      runState.pendingImageData = { base64: data.base64, mimeType: data.mimeType };
    }
  }

  // Max iterations reached without terminal tool → escalate
  if (runState.toolCallsMade >= config.maxToolCalls && runState.decision !== 'escalate') {
    await forceEscalate(deps, input, 'max_tool_calls_exceeded', {
      promptTokens: 0,
      completionTokens: 0,
      usedModelName: model,
      latencyMs: 0,
      callCost: 0,
    });
    runState.decision = 'escalate';
    runState.toolCallsMade++;
  }

  return toResult(runState);
}

// ─── Helpers ──────────────────────────────────────────────────────────────────

function toolToDecision(tool: ToolName): 'auto' | 'escalate' | 'ask' | 'propose' {
  if (tool === 'escalate_to_human') return 'escalate';
  if (tool === 'close_ticket') return 'auto';
  if (tool === 'propose_resolution' || tool === 'propose_refund') return 'propose';
  return 'ask';
}

function toResult(state: AgentRunState): SupportAgentRunResult {
  return {
    decision: state.decision,
    confidence: state.currentConfidence,
    toolCallsMade: state.toolCallsMade,
    totalCostUsd: state.cumulativeCostUsd,
    escalated: state.decision === 'escalate',
  };
}

async function forceEscalate(
  deps: AgentDeps,
  input: SupportAgentRunInput,
  reason: string,
  meta: {
    promptTokens: number;
    completionTokens: number;
    usedModelName: string;
    latencyMs: number;
    callCost: number;
  },
): Promise<void> {
  const { db, piiKey, runState } = deps;
  const escalateInput = {
    parentType: input.parentType,
    parentId: input.parentId,
    reason,
    confidence: runState.currentConfidence,
  };
  // Execute escalate_to_human tool if schema is valid
  const schema = TOOL_SCHEMAS['escalate_to_human'];
  const parsed = schema.safeParse(escalateInput);
  if (parsed.success) {
    await TOOL_IMPLS['escalate_to_human'](deps, escalateInput).catch((err) => {
      captureCaught(err, { scope: 'server.ai.agents.support.agent', severity: 'info' });
      // Best-effort
    });
  }
  await interventionsQ
    .insertEncrypted(
      db,
      {
        parentType: input.parentType,
        parentId: input.parentId,
        agentName: SUPPORT_AGENT_NAME,
        toolName: 'escalate_to_human',
        confidence: String(runState.currentConfidence),
        decision: 'escalate',
        tokensIn: meta.promptTokens,
        tokensOut: meta.completionTokens,
        latencyMs: meta.latencyMs,
        costUsd: String(meta.callCost.toFixed(6)),
      },
      JSON.stringify(escalateInput),
      JSON.stringify({ forced: true, reason }),
      piiKey,
    )
    .catch((err) => {
      captureCaught(err, { scope: 'server.ai.agents.support.agent', severity: 'info' });
      // Best-effort telemetry
    });
}

// ─── AgentDeps factory ────────────────────────────────────────────────────────

/**
 * Assembles AgentDeps from a minimal env + input.
 * Loads support config from DB, builds an empty redaction map (no PII loaded
 * at this stage — the agent loop refines it if needed), and creates Gemini client.
 *
 * @param env  Must include DATABASE_URL, PII_KEY, and GOOGLE_API_KEY.
 * @param input  The trigger input (parentType / parentId / trigger).
 */
export async function makeSupportAgentDeps(
  env: { DATABASE_URL: string; PII_KEY: string; GOOGLE_API_KEY: string },
  input: SupportAgentRunInput,
): Promise<AgentDeps> {
  const db = createDbService({ DATABASE_URL: env.DATABASE_URL });

  // Load all support config keys in parallel
  const [
    llmModel,
    fallbackModel,
    maxToolCalls,
    maxCostCents,
    notificationEmailsRaw,
    slaAiHours,
    slaHumanHours,
    vendorWindowHours,
    autocloseDays,
    triageModel,
    decisionModel,
  ] = await Promise.all([
    configQ.getSystemConfig(db, 'llm_model'),
    configQ.getSystemConfig(db, 'llm_fallback_model'),
    configQ.getSystemConfig(db, 'support_ai_max_tool_calls'),
    configQ.getSystemConfig(db, 'support_ai_max_cost_cents'),
    configQ.getSystemConfig(db, 'support_notification_emails'),
    configQ.getSystemConfig(db, 'support_sla_ai_hours'),
    configQ.getSystemConfig(db, 'support_sla_human_hours'),
    configQ.getSystemConfig(db, 'support_vendor_window_hours'),
    configQ.getSystemConfig(db, 'support_autoclose_days'),
    configQ.getSystemConfig(db, 'support_llm_triage_model'),
    configQ.getSystemConfig(db, 'support_llm_decision_model'),
  ]);

  let notificationEmails: string[];
  try {
    notificationEmails = JSON.parse(notificationEmailsRaw) as string[];
  } catch (err) {
    captureCaught(err, { scope: 'server.ai.agents.support.agent', severity: 'warning' });
    notificationEmails = [];
  }

  const config: SupportConfigSnapshot = {
    maxToolCalls: parseInt(maxToolCalls, 10) || 12,
    maxCostCents: parseInt(maxCostCents, 10) || 50,
    notificationEmails,
    slaAiHours: parseInt(slaAiHours, 10) || 2,
    slaHumanHours: parseInt(slaHumanHours, 10) || 24,
    vendorWindowHours: parseInt(vendorWindowHours, 10) || 48,
    autocloseDays: parseInt(autocloseDays, 10) || 7,
    triageModel: requireConfiguredLlmModel(triageModel || llmModel, 'support triage'),
    decisionModel: requireConfiguredLlmModel(decisionModel || llmModel, 'support decision'),
  };

  const model = config.triageModel;
  const configuredFallbackModel = requireConfiguredLlmModel(fallbackModel, 'support fallback');

  const provider: LlmProvider = geminiProvider;
  const creds: AiCredentials = { apiKey: env.GOOGLE_API_KEY };

  return {
    db,
    provider,
    creds,
    piiKey: env.PII_KEY,
    parent: { type: input.parentType, id: input.parentId },
    locale: 'he', // default; agent may detect from ticket opener
    config,
    model,
    fallbackModel: configuredFallbackModel,
    redactionMap: buildRedactionMap(new Map(), new Map()),
    runState: {
      cumulativeCostUsd: 0,
      toolCallsMade: 0,
      currentConfidence: 1,
      pendingImageData: null,
      decision: 'ask',
    },
  };
}

// ─── GeminiSupportAgent class ──────────────────────────────────────────────────

/**
 * Gemini-backed support agent.
 *
 * Registered via getAiAgents() when GOOGLE_API_KEY is present.
 * Constructs its own GeminiClient and AgentDeps from the provided env + db.
 */
export class GeminiSupportAgent implements SupportAgent {
  constructor(
    _provider: LlmProvider,
    _creds: AiCredentials,
    private readonly defaultModel?: string,
  ) {}

  async run(input: SupportAgentRunInput): Promise<SupportAgentRunResult> {
    // GeminiSupportAgent.run() requires DB + PII context not available in registry.
    // Use makeSupportAgentDeps() + runSupportAgent() directly via the outbox handler.
    throw new Error(
      'GeminiSupportAgent.run() requires AgentDeps. Use makeSupportAgentDeps() + runSupportAgent() instead. ' +
        `model: ${this.defaultModel ?? 'default'}, ` +
        `input: ${JSON.stringify(input)}`,
    );
  }
}
