/**
 * Definition-driven CS agent runner (Wave 3).
 *
 * Loads agent_definitions from DB, runs knowledge adapters + escalation rules,
 * calls the chained LLM provider, executes decisions, and publishes live updates.
 */

import { eq } from 'drizzle-orm';
import type { SupportAgentRunInput, SupportAgentRunResult } from '@/server/ai/types';
import { computeCostUsd } from '@/server/ai/pricing';
import { buildChainedProvider } from '@/server/ai/providers/chained';
import type { LlmChainConfig } from '@/server/ai/providers/chained';
import type { AiCredentials } from '@/server/ai/credentials';
import { getDb } from '@/server/db/client';
import {
  getAgentDefinition,
  getAgentDefinitionBySlug,
} from '@/server/db/queries/agent-definitions';
import { getLlmQueue } from '@/server/db/queries/llm-queues.js';
import * as interventionsQ from '@/server/db/queries/ai-interventions';
import * as ticketQ from '@/server/db/queries/support-tickets';
import * as caseQ from '@/server/db/queries/support-cases';
import * as msgQ from '@/server/db/queries/support-messages';
import * as transitionsQ from '@/server/db/queries/support-state-transitions';
import { insertOutboxRow } from '@/server/db/queries/outbox';
import { supportTickets, transactionCases } from '@/server/db/schema';
import type { AgentDefinition } from '@/server/db/schema';
import type { MultidealEnv } from '@/server/env';
import { withSentry } from '@/server/observability/with-sentry';
import { captureCaught } from '@/server/observability/capture.server';
import { publishLive } from '@/server/notifications/send';
import { liveCaseCh, liveTicketCh } from '@/server/notifications/topics';
import { KNOWLEDGE_ADAPTERS } from '@/server/support/adapters/knowledge/registry';
import type { KnowledgeAdapterId, KnowledgeChunk } from '@/server/support/adapters/knowledge/types';
import { getDecision } from '@/server/support/decisions/registry';
import type { DecisionId } from '@/server/support/decisions/types';
import { listDecryptedByParent } from '@/server/support/messages';
import { ESCALATION_RULES } from '@/server/support/rules/registry';
import type { EscalationRuleId } from '@/server/support/rules/types';
import type { AdapterContext, EscalationContext } from '@/server/support/types';
import { asCaseId } from '@/server/platform-seams/ids.js';

// ─── Constants ────────────────────────────────────────────────────────────────

const TICKET_TERMINAL = new Set(['resolved', 'closed']);
const CASE_TERMINAL = new Set(['resolved', 'closed', 'human_review']);
const DEFAULT_DEFINITION_SLUG = 'customer-support';
const AGENT_NAME = 'support_agent';

const ADAPTER_PLACEHOLDERS: Record<KnowledgeAdapterId, string> = {
  'user-profile': 'user_name',
  'order-history': 'order_history',
  'order-context': 'order_context',
  'deal-info': 'deal_info',
  'vendor-profile': 'vendor_profile',
  'vendor-payouts': 'vendor_payouts',
  'return-policy': 'return_policy',
  'affiliate-history': 'affiliate_history',
  'kb-search': 'kb_results',
  attachments: 'attachments',
  'platform-status': 'platform_status',
};

// ─── Types ────────────────────────────────────────────────────────────────────

export type RunnerEnv = Pick<MultidealEnv, 'DATABASE_URL' | 'PII_KEY' | 'AI'> & {
  TOPIC_DO?: MultidealEnv['TOPIC_DO'];
};

interface AgentTurn {
  decision: DecisionId;
  confidence: number;
  message?: string;
  reasoning?: string;
  amountCents?: number;
}

interface RunState {
  cumulativeCostUsd: number;
  turnCount: number;
  toolCallsMade: number;
  currentConfidence: number;
  decision: SupportAgentRunResult['decision'];
  escalated: boolean;
}

// ─── Entry point ──────────────────────────────────────────────────────────────

export const runSupportAgent = withSentry(runSupportAgentImpl, {
  name: 'support.agent.run',
  kind: 'ai-agent',
});

async function runSupportAgentImpl(
  env: RunnerEnv,
  input: SupportAgentRunInput,
): Promise<SupportAgentRunResult> {
  const db = getDb({ DATABASE_URL: env.DATABASE_URL });
  const piiKey = env.PII_KEY;

  const parent = await loadParent(db, input);
  if (!parent) {
    return emptyResult();
  }

  const definition = await resolveDefinition(db, parent.agentDefinitionId);
  let chainConfig: LlmChainConfig;
  if (definition.llmChain?.chain?.length) {
    chainConfig = definition.llmChain as LlmChainConfig;
  } else {
    const supportQueue = await getLlmQueue(db, 'support');
    if (!supportQueue?.chain?.length) {
      throw new Error(`No LLM chain for "${definition.slug}" and support queue is also empty`);
    }
    chainConfig = { chain: supportQueue.chain };
  }

  let provider: Awaited<ReturnType<typeof buildChainedProvider>>;
  try {
    provider = await buildChainedProvider(db, piiKey, chainConfig);
  } catch (err) {
    captureCaught(err, { scope: 'support.agent-runner.no_usable_provider', severity: 'warning' });
    const state: RunState = {
      cumulativeCostUsd: 0,
      turnCount: 0,
      toolCallsMade: 0,
      currentConfidence: 0,
      decision: 'escalate',
      escalated: false,
    };
    await executeEscalation(db, piiKey, input, 'no_usable_provider', state);
    return toResult(state);
  }

  const adapterCtx: AdapterContext = {
    userId: parent.userId,
    ticketId: input.parentId,
    parentType: input.parentType,
    parentId: input.parentId,
    locale: parent.locale,
    db,
    env: env as MultidealEnv,
  };

  const knowledgeChunks = await runKnowledgeAdapters(definition.knowledgeAdapterIds, adapterCtx);
  const messages = await listDecryptedByParent(db, input.parentType, input.parentId, piiKey, {
    visibilities: ['public'],
  });

  const conversation = messages.map((m) => ({
    role: authorToRole(m.authorType),
    content: m.body,
  }));

  const state: RunState = {
    cumulativeCostUsd: 0,
    turnCount: 0,
    toolCallsMade: 0,
    currentConfidence: 1,
    decision: 'ask',
    escalated: false,
  };

  const definitionCtx = {
    aiConfidenceThreshold: Number(definition.aiConfidenceThreshold),
    maxCostCents: definition.maxCostCents,
    maxToolCalls: definition.maxToolCalls,
    escalationRuleIds: definition.escalationRuleIds,
  };

  const preEscalation = await checkEscalationRules(definition.escalationRuleIds, {
    parentType: input.parentType,
    parentId: input.parentId,
    conversation,
    aiDraft: '',
    confidence: 1,
    definition: definitionCtx,
    knowledgeChunks,
    currentCostCents: 0,
    currentToolCalls: 0,
    turnCount: 0,
  });

  if (preEscalation) {
    await executeEscalation(db, piiKey, input, preEscalation.reason, state);
    return toResult(state);
  }

  const systemPrompt = buildSystemPrompt(definition, knowledgeChunks, conversation);

  while (state.toolCallsMade < definition.maxToolCalls) {
    const loopStart = Date.now();
    const prompt = buildTurnPrompt(systemPrompt, conversation, state.turnCount);

    const resp = await provider.generateText({} as AiCredentials, {
      model: '',
      prompt,
      kind: 'SUPPORT',
    });

    const latencyMs = Date.now() - loopStart;
    const callCost =
      resp.costUsd ??
      computeCostUsd(
        chainConfig.chain[0]?.model ?? '',
        resp.usage.promptTokens,
        resp.usage.completionTokens,
      ) ??
      0;

    state.cumulativeCostUsd += callCost;
    state.turnCount++;

    const turn = parseTurn(resp.text, definition.availableDecisions);
    if (!turn) {
      if (state.turnCount >= 2) {
        await executeEscalation(db, piiKey, input, 'invalid_json', state);
        break;
      }
      continue;
    }

    state.currentConfidence = turn.confidence;

    const postEscalation = await checkEscalationRules(definition.escalationRuleIds, {
      parentType: input.parentType,
      parentId: input.parentId,
      conversation,
      aiDraft: turn.message ?? resp.text,
      confidence: turn.confidence,
      definition: definitionCtx,
      knowledgeChunks,
      currentCostCents: Math.ceil(state.cumulativeCostUsd * 100),
      currentToolCalls: state.toolCallsMade,
      turnCount: state.turnCount,
    });

    if (postEscalation) {
      await recordIntervention(db, piiKey, input, definition, {
        decision: 'escalate',
        toolName: postEscalation.ruleId,
        confidence: turn.confidence,
        tokensIn: resp.usage.promptTokens,
        tokensOut: resp.usage.completionTokens,
        latencyMs,
        callCost,
        input: { reason: postEscalation.reason },
        output: { escalated: true },
      });
      await executeEscalation(db, piiKey, input, postEscalation.reason, state);
      break;
    }

    const decisionMeta = getDecision(turn.decision);
    if (
      !decisionMeta.requiresHumanApproval &&
      turn.confidence < definitionCtx.aiConfidenceThreshold &&
      turn.decision !== 'escalate'
    ) {
      await executeEscalation(
        db,
        piiKey,
        input,
        `confidence ${turn.confidence} below threshold ${definitionCtx.aiConfidenceThreshold}`,
        state,
      );
      break;
    }

    const execResult = await executeDecision(db, env, piiKey, input, turn, state, definition);
    state.toolCallsMade++;
    state.decision = decisionToResult(turn.decision);

    await recordIntervention(db, piiKey, input, definition, {
      decision: state.decision,
      toolName: turn.decision,
      confidence: turn.confidence,
      tokensIn: resp.usage.promptTokens,
      tokensOut: resp.usage.completionTokens,
      latencyMs,
      callCost,
      input: turn,
      output: execResult,
    });

    if (execResult.messageId && env.TOPIC_DO) {
      const channel =
        input.parentType === 'ticket' ? liveTicketCh(input.parentId) : liveCaseCh(input.parentId);
      await publishLive({
        channel,
        event: 'support_ticket.ai_reply',
        payload: {
          msgId: execResult.messageId,
          parentType: input.parentType,
          parentId: input.parentId,
          authorType: 'ai',
          body: turn.message ?? '',
          ts: new Date().toISOString(),
        },
        ctx: { env: env as MultidealEnv },
      }).catch((err) => {
        captureCaught(err, { scope: 'support.agent-runner.publishLive', severity: 'warning' });
      });
    }

    if (execResult.terminal) {
      break;
    }

    if (turn.message) {
      conversation.push({ role: 'assistant', content: turn.message });
    }
  }

  if (state.toolCallsMade >= definition.maxToolCalls && !state.escalated) {
    await executeEscalation(db, piiKey, input, 'max_tool_calls_exceeded', state);
  }

  return toResult(state);
}

// ─── Definition + parent loading ─────────────────────────────────────────────

interface ParentContext {
  userId: string;
  locale: 'he' | 'en';
  agentDefinitionId: string | null;
}

async function loadParent(
  db: ReturnType<typeof getDb>,
  input: SupportAgentRunInput,
): Promise<ParentContext | null> {
  if (input.parentType === 'ticket') {
    const ticket = await ticketQ.findById(db, input.parentId);
    if (!ticket || TICKET_TERMINAL.has(ticket.status)) return null;
    return {
      userId: ticket.openerId,
      locale: 'he',
      agentDefinitionId: ticket.agentDefinitionId,
    };
  }

  const c = await caseQ.findById(db, input.parentId);
  if (!c || CASE_TERMINAL.has(c.status)) return null;
  return {
    userId: c.customerId,
    locale: 'he',
    agentDefinitionId: c.agentDefinitionId,
  };
}

async function resolveDefinition(
  db: ReturnType<typeof getDb>,
  agentDefinitionId: string | null,
): Promise<AgentDefinition> {
  if (agentDefinitionId) {
    const row = await getAgentDefinition(db, agentDefinitionId);
    if (row) return row;
  }
  const fallback = await getAgentDefinitionBySlug(db, DEFAULT_DEFINITION_SLUG);
  if (!fallback) {
    throw new Error(`Agent definition not found: ${DEFAULT_DEFINITION_SLUG}`);
  }
  return fallback;
}

// ─── Knowledge adapters ───────────────────────────────────────────────────────

async function runKnowledgeAdapters(
  adapterIds: string[],
  ctx: AdapterContext,
): Promise<KnowledgeChunk[]> {
  const tasks = adapterIds.map(async (id) => {
    const adapter = KNOWLEDGE_ADAPTERS[id as KnowledgeAdapterId];
    if (!adapter) {
      console.warn(`agent-runner: unknown knowledge adapter: ${id}`);
      return null;
    }
    try {
      return await adapter.fetch(ctx);
    } catch (err) {
      captureCaught(err, { scope: 'support.agent-runner.adapter', severity: 'warning' });
      return null;
    }
  });
  const results = await Promise.all(tasks);
  return results.filter((c): c is KnowledgeChunk => c !== null);
}

function placeholderValue(chunk: KnowledgeChunk): string {
  if (chunk.adapterId === 'user-profile') {
    const name = chunk.data.displayName;
    if (typeof name === 'string' && name.length > 0) return name;
  }
  return chunk.summary || chunk.error || '(אין מידע)';
}

function buildSystemPrompt(
  definition: AgentDefinition,
  chunks: KnowledgeChunk[],
  conversation: Array<{ role: string; content: string }>,
): string {
  let prompt = definition.systemPromptTemplate;

  for (const chunk of chunks) {
    const key = ADAPTER_PLACEHOLDERS[chunk.adapterId];
    if (key) {
      prompt = prompt.replaceAll(`{{${key}}}`, placeholderValue(chunk));
    }
  }

  // Clear any remaining placeholders
  prompt = prompt.replace(/\{\{[a-z_]+\}\}/g, '(לא זמין)');

  const decisions = definition.availableDecisions
    .map((id) => {
      const d = getDecision(id as DecisionId);
      return `- ${d.id}: ${d.description}`;
    })
    .join('\n');

  const history =
    conversation.length > 0
      ? conversation.map((m) => `[${m.role}]: ${m.content}`).join('\n')
      : '(אין הודעות קודמות)';

  return [
    prompt,
    '',
    '## החלטות זמינות',
    decisions,
    '',
    '## פורמט תגובה',
    'החזר JSON בלבד:',
    '{"decision": "<decision_id>", "confidence": <0-1>, "message": "<טקסט ללקוח אם רלוונטי>", "reasoning": "<נימוק>"}',
    '',
    '## היסטוריית שיחה',
    history,
  ].join('\n');
}

function buildTurnPrompt(
  systemPrompt: string,
  conversation: Array<{ role: string; content: string }>,
  turnCount: number,
): string {
  if (turnCount === 0) return systemPrompt;
  const lastUser = [...conversation].reverse().find((m) => m.role === 'user');
  return [
    systemPrompt,
    '',
    `[system]: המשך לפי ההחלטה המתאימה.`,
    lastUser ? `[user]: ${lastUser.content}` : '',
  ].join('\n');
}

// ─── Escalation ───────────────────────────────────────────────────────────────

async function checkEscalationRules(
  ruleIds: string[],
  ctx: EscalationContext,
): Promise<{ ruleId: string; reason: string } | null> {
  for (const id of ruleIds) {
    const rule = ESCALATION_RULES[id as EscalationRuleId];
    if (!rule) continue;
    const signal = await rule.check(ctx);
    if (signal) {
      return { ruleId: signal.ruleId, reason: signal.reason };
    }
  }
  return null;
}

async function executeEscalation(
  db: ReturnType<typeof getDb>,
  piiKey: string,
  input: SupportAgentRunInput,
  reason: string,
  state: RunState,
): Promise<void> {
  state.decision = 'escalate';
  state.escalated = true;

  if (input.parentType === 'ticket') {
    const ticket = await ticketQ.findById(db, input.parentId);
    if (!ticket) return;

    await db.transaction(async (tx) => {
      await tx
        .update(supportTickets)
        .set({ status: 'awaiting_agent', updatedAt: new Date() })
        .where(eq(supportTickets.id, input.parentId));

      await transitionsQ.insert(tx as never, {
        parentType: 'ticket',
        parentId: input.parentId,
        fromState: ticket.status,
        toState: 'awaiting_agent',
        actorType: 'ai',
        actorId: null,
        reason,
        metadata: {},
      });

      await insertOutboxRow(tx as never, {
        aggregateType: 'ticket',
        aggregateId: input.parentId,
        eventType: 'support.notif.email',
        payload: {
          ticketId: input.parentId,
          templateKey: 'escalated_to_human',
          recipients: ['site_support'],
        },
      });
    });
  } else {
    const c = await caseQ.findById(db, input.parentId);
    if (!c) return;

    await db.transaction(async (tx) => {
      await tx
        .update(transactionCases)
        .set({ status: 'human_review', updatedAt: new Date() })
        .where(eq(transactionCases.id, asCaseId(input.parentId)));

      await transitionsQ.insert(tx as never, {
        parentType: 'case',
        parentId: input.parentId,
        fromState: c.status,
        toState: 'human_review',
        actorType: 'ai',
        actorId: null,
        reason,
        metadata: {},
      });

      await insertOutboxRow(tx as never, {
        aggregateType: 'case',
        aggregateId: input.parentId,
        eventType: 'support.notif.email',
        payload: {
          caseId: input.parentId,
          templateKey: 'escalated_to_human',
          recipients: ['customer', 'vendor', 'site_support'],
        },
      });
    });
  }

  await interventionsQ
    .insertEncrypted(
      db,
      {
        parentType: input.parentType,
        parentId: input.parentId,
        agentName: AGENT_NAME,
        toolName: 'escalate',
        confidence: String(state.currentConfidence),
        decision: 'escalate',
        tokensIn: null,
        tokensOut: null,
        latencyMs: null,
        costUsd: null,
      },
      JSON.stringify({ reason }),
      JSON.stringify({ escalated: true }),
      piiKey,
    )
    .catch((err) => {
      captureCaught(err, { scope: 'support.agent-runner.escalate', severity: 'info' });
    });
}

// ─── Decision execution ───────────────────────────────────────────────────────

interface ExecResult {
  terminal: boolean;
  messageId?: string;
}

async function executeDecision(
  db: ReturnType<typeof getDb>,
  _env: RunnerEnv,
  piiKey: string,
  input: SupportAgentRunInput,
  turn: AgentTurn,
  state: RunState,
  _definition: AgentDefinition,
): Promise<ExecResult> {
  switch (turn.decision) {
    case 'escalate':
      await executeEscalation(db, piiKey, input, turn.reasoning ?? 'agent_escalate', state);
      return { terminal: true };

    case 'reply':
    case 'request-info': {
      const text = turn.message?.trim();
      if (!text) {
        return { terminal: false };
      }
      const msg = await msgQ.insertEncrypted(
        db,
        {
          parentType: input.parentType,
          parentId: input.parentId,
          authorType: 'ai',
          authorId: null,
          visibility: 'public',
        },
        text,
        piiKey,
      );
      return { terminal: false, messageId: msg.id };
    }

    case 'close': {
      if (input.parentType !== 'ticket') {
        await executeEscalation(db, piiKey, input, 'close_not_allowed_on_case', state);
        return { terminal: true };
      }
      const ticket = await ticketQ.findById(db, input.parentId);
      if (!ticket) return { terminal: true };

      const resolution = turn.message ?? turn.reasoning ?? 'resolved by AI';
      await db.transaction(async (tx) => {
        await tx
          .update(supportTickets)
          .set({
            status: 'resolved',
            resolvedAt: new Date(),
            updatedAt: new Date(),
            metadata: {
              ...(ticket.metadata as Record<string, unknown>),
              resolution,
              resolvedByAi: true,
            },
          })
          .where(eq(supportTickets.id, input.parentId));

        await transitionsQ.insert(tx as never, {
          parentType: 'ticket',
          parentId: input.parentId,
          fromState: ticket.status,
          toState: 'resolved',
          actorType: 'ai',
          actorId: null,
          reason: resolution,
          metadata: {},
        });
      });
      state.decision = 'auto';
      return { terminal: true };
    }

    case 'propose-refund': {
      // Recommendation-only fallback
      state.decision = 'propose';
      if (turn.message) {
        const msg = await msgQ.insertEncrypted(
          db,
          {
            parentType: input.parentType,
            parentId: input.parentId,
            authorType: 'ai',
            authorId: null,
            visibility: input.parentType === 'case' ? 'vendor_internal' : 'public',
          },
          turn.message,
          piiKey,
        );
        return { terminal: false, messageId: msg.id };
      }
      return { terminal: false };
    }

    default: {
      const meta = getDecision(turn.decision);
      if (meta.requiresHumanApproval) {
        state.decision = 'propose';
        if (turn.message) {
          const msg = await msgQ.insertEncrypted(
            db,
            {
              parentType: input.parentType,
              parentId: input.parentId,
              authorType: 'ai',
              authorId: null,
              visibility: input.parentType === 'case' ? 'vendor_internal' : 'public',
            },
            turn.message,
            piiKey,
          );
          return { terminal: false, messageId: msg.id };
        }
        return { terminal: false };
      }
      return { terminal: false };
    }
  }
}

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

function parseTurn(text: string, allowedDecisions: string[]): AgentTurn | null {
  try {
    const cleaned = text
      .replace(/^```(?:json)?\s*/i, '')
      .replace(/\s*```\s*$/i, '')
      .trim();
    const parsed = JSON.parse(cleaned) as Record<string, unknown>;
    if (
      typeof parsed.decision === 'string' &&
      allowedDecisions.includes(parsed.decision) &&
      typeof parsed.confidence === 'number'
    ) {
      return {
        decision: parsed.decision as DecisionId,
        confidence: parsed.confidence,
        message: typeof parsed.message === 'string' ? parsed.message : undefined,
        reasoning: typeof parsed.reasoning === 'string' ? parsed.reasoning : undefined,
        amountCents: typeof parsed.amountCents === 'number' ? parsed.amountCents : undefined,
      };
    }
    return null;
  } catch (err) {
    captureCaught(err, { scope: 'support.agent-runner.parseTurn', severity: 'warning' });
    return null;
  }
}

function authorToRole(authorType: string): string {
  if (authorType === 'customer' || authorType === 'vendor') return 'user';
  if (authorType === 'ai' || authorType === 'human_agent' || authorType === 'admin')
    return 'assistant';
  return 'system';
}

function decisionToResult(decision: DecisionId): SupportAgentRunResult['decision'] {
  if (decision === 'escalate') return 'escalate';
  if (decision === 'close') return 'auto';
  if (decision.startsWith('propose-')) return 'propose';
  return 'ask';
}

async function recordIntervention(
  db: ReturnType<typeof getDb>,
  piiKey: string,
  input: SupportAgentRunInput,
  definition: AgentDefinition,
  meta: {
    decision: SupportAgentRunResult['decision'];
    toolName: string;
    confidence: number;
    tokensIn: number;
    tokensOut: number;
    latencyMs: number;
    callCost: number;
    input: unknown;
    output: unknown;
  },
): Promise<void> {
  await interventionsQ.insertEncrypted(
    db,
    {
      parentType: input.parentType,
      parentId: input.parentId,
      agentName: AGENT_NAME,
      agentDefVersion: definition.version,
      toolName: meta.toolName,
      confidence: String(meta.confidence),
      decision: meta.decision,
      tokensIn: meta.tokensIn,
      tokensOut: meta.tokensOut,
      latencyMs: meta.latencyMs,
      costUsd: String(meta.callCost.toFixed(6)),
    },
    JSON.stringify(meta.input),
    JSON.stringify(meta.output),
    piiKey,
  );
}

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

function emptyResult(): SupportAgentRunResult {
  return {
    decision: 'ask',
    confidence: 1,
    toolCallsMade: 0,
    totalCostUsd: 0,
    escalated: false,
  };
}
