import type { FastifyBaseLogger } from 'fastify';
import type postgres from 'postgres';
import type { Server as SocketServer } from 'socket.io';
import type { Channel, ChannelSettings, Ticket, TicketIntake } from '@wa-ticketing/shared';
import { channelAiOfflineMessage, HUMAN_CONFIRM_PROMPT, mergeTicketIntake } from '@wa-ticketing/shared';
import { sql } from '../db.js';
import { messagingService } from './messagingService.js';
import { auditService } from './auditService.js';
import type { OpenAiChatMessage } from './openaiService.js';
import { companyService } from './companyService.js';
import { knowledgeModuleService } from './knowledgeModuleService.js';
import { aiAgentService, type AiAgentSecretRow } from './aiAgentService.js';
import { completeChat, platformChatCompletionText } from './llmProviderService.js';
import { sendAutomatedOutbound } from './automatedOutboundService.js';
import {
  isUnableToAssistReply,
  markAwaitingHumanConfirm,
  processHumanRequestInbound,
} from './humanRequestService.js';
import { ticketService } from './ticketService.js';
import { ticketImageAsDataUrl, transcribeTicketVoiceNote } from './ticketMediaService.js';
import { schedulingIntentService } from './schedulingIntentService.js';

const KB_MAX = 12_000;
/** Leave headroom for prompts / intake JSON inside the same model context budget */
const CHANNEL_KB_CAP = 6_000;

function nextOnboardingStep(
  settings: ChannelSettings,
  intake: TicketIntake,
  inboundText: string,
): { intake: TicketIntake; outgoing: string | null; advanced: boolean } {
  const t = settings.onboarding.templates;

  if (intake.step === 'idle') {
    const out = t.whoAreYou.trim();
    if (!out) return { intake, outgoing: null, advanced: false };
    return { intake: { step: 'who', answers: { ...intake.answers } }, outgoing: out, advanced: true };
  }

  if (intake.step === 'who') {
    const out = t.company.trim();
    if (!out) return { intake, outgoing: null, advanced: false };
    return {
      intake: { step: 'company', answers: { ...intake.answers, whoAreYou: inboundText.trim() } },
      outgoing: out,
      advanced: true,
    };
  }

  if (intake.step === 'company') {
    const out = t.query.trim();
    if (!out) return { intake, outgoing: null, advanced: false };
    return {
      intake: { step: 'query', answers: { ...intake.answers, company: inboundText.trim() } },
      outgoing: out,
      advanced: true,
    };
  }

  if (intake.step === 'query') {
    const out = t.queryStatus.trim();
    if (!out) return { intake, outgoing: null, advanced: false };
    return {
      intake: { step: 'status', answers: { ...intake.answers, query: inboundText.trim() } },
      outgoing: out,
      advanced: true,
    };
  }

  if (intake.step === 'status') {
    return {
      intake: {
        step: 'done',
        answers: { ...intake.answers, queryStatus: inboundText.trim() },
      },
      outgoing: null,
      advanced: true,
    };
  }

  return { intake, outgoing: null, advanced: false };
}

async function runAiReply(opts: {
  io: SocketServer;
  log: FastifyBaseLogger;
  ticketId: string;
  channelId: string;
  contactPhone: string;
  settings: ChannelSettings;
  intake: TicketIntake;
  companyKnowledge: { text: string; titles: string[] };
  /** When set and key present, overrides channel model/credentials */
  agent: AiAgentSecretRow | null;
}): Promise<void> {
  const { io, log, ticketId, channelId, contactPhone, settings, intake, companyKnowledge, agent } = opts;
  const msgs = await messagingService.getMessages(ticketId);
  const recent = msgs.slice(-20);
  // Build OpenAI messages; inbound images become multimodal parts so vision models can see them
  const chatMsgs: OpenAiChatMessage[] = [];
  for (const m of recent) {
    const role = m.direction === 'inbound' ? ('user' as const) : ('assistant' as const);
    if (role === 'assistant') {
      chatMsgs.push({ role, content: m.content });
      continue;
    }
    if ((m.message_type ?? 'text') === 'image' && m.media_path) {
      const dataUrl = await ticketImageAsDataUrl(m.media_path);
      const caption = m.content?.trim() && m.content !== '[Image]' ? m.content.trim() : 'Customer sent an image.';
      if (dataUrl) {
        chatMsgs.push({
          role,
          content: [
            { type: 'text', text: caption },
            { type: 'image_url', image_url: { url: dataUrl } },
          ],
        });
        continue;
      }
      chatMsgs.push({ role, content: `${caption} (image attached but could not be loaded for AI)` });
      continue;
    }
    if ((m.message_type ?? 'text') === 'voice_note' && m.media_path) {
      const transcript = await transcribeTicketVoiceNote(m.media_path);
      if (transcript) {
        chatMsgs.push({
          role,
          content: `Customer sent a voice note. Transcript: "${transcript}"`,
        });
        continue;
      }
      chatMsgs.push({
        role,
        content: 'Customer sent a voice note (transcription unavailable — ask them to type the details).',
      });
      continue;
    }
    if ((m.message_type ?? 'text') !== 'text') {
      chatMsgs.push({ role, content: `[${m.message_type}] ${m.content}` });
      continue;
    }
    chatMsgs.push({ role, content: m.content });
  }

  const channelKb = settings.ai.knowledgeBaseText.slice(0, CHANNEL_KB_CAP);
  const orgBudget = Math.max(0, KB_MAX - channelKb.length);
  const orgKb = companyKnowledge.text.slice(0, orgBudget);
  const topicList = companyKnowledge.titles.join(', ');
  const ctx = JSON.stringify(intake.answers);
  const guardrail = `You must answer only from the "Channel knowledge base" and "Organisation knowledge" sections below. The organisation has enabled these topics: ${topicList}. If the user's message is not covered by those sections or falls outside those topics, reply briefly that you cannot assist with that topic and ask whether they would like to speak to a human agent (yes/no). Do not use outside knowledge or invent facts.`;

  const personaBase = agent?.api_key?.trim()
    ? [agent.persona_system.trim(), agent.guardrails.trim()].filter(Boolean).join('\n\n')
    : settings.ai.systemPrompt.trim();

  const system = `${personaBase}

${guardrail}

Channel knowledge base:
${channelKb}

Organisation knowledge (enabled topics):
${orgKb}

Structured intake (if any):
${ctx}`;

  try {
    let reply: string;
    if (agent?.api_key?.trim()) {
      reply = await completeChat({
        provider: agent.provider,
        apiKey: agent.api_key,
        baseUrl: agent.base_url,
        model: agent.model_id,
        system,
        messages: chatMsgs,
      });
    } else {
      reply = await platformChatCompletionText({
        model: settings.ai.model,
        system,
        messages: chatMsgs,
      });
    }
    if (!reply.trim()) return;

    let outbound = reply.trim();
    if (isUnableToAssistReply(outbound)) {
      outbound = HUMAN_CONFIRM_PROMPT;
      await markAwaitingHumanConfirm(ticketId, intake);
      io.to(`ticket:${ticketId}`).emit('ticket:updated', {
        ticket: { id: ticketId, intake: { ...intake, awaiting_human_confirm: true } },
      });
    }

    await sendAutomatedOutbound({ io, ticketId, channelId, contactPhone, text: outbound });
  } catch (err) {
    log.error({ err, ticketId }, '[talkback] LLM reply failed');
    await auditService.log(null, 'system', 'talkback.ai_failed', 'ticket', ticketId, { error: String(err) });
    const offline = channelAiOfflineMessage(settings);
    if (offline) {
      await sendAutomatedOutbound({ io, ticketId, channelId, contactPhone, text: offline });
    }
  }
}

export const talkbackService = {
  async handleInbound(opts: {
    io: SocketServer;
    log: FastifyBaseLogger;
    ticket: Ticket;
    channel: Channel;
    contactPhone: string;
    inboundText: string;
    /** Inbound message id (for calendar suggestion linkage) */
    messageId?: string | null;
  }): Promise<void> {
    const { io, log, ticket, channel, contactPhone, inboundText, messageId } = opts;
    // Inbound reached the API only if the worker has a session; DB status can lag behind briefly.
    const settings = channel.settings;
    if (!settings) return;

    // TransactionSql typings omit template-tag call signature; cast via unknown.
    const { outgoing, intakeAfter: intakeFromOnboarding, sentOnboardingTemplate } = await sql.begin(async txn => {
      const q = txn as unknown as postgres.Sql;
      let outgoingMsg: string | null = null;
      let sentTpl = false;
      let intakeAfterInner: TicketIntake = mergeTicketIntake(ticket.intake);

      const [row] = await q<[Ticket]>`
        SELECT * FROM tickets WHERE id = ${ticket.id} FOR UPDATE
      `;
      if (!row) return { outgoing: null, intakeAfter: intakeAfterInner, sentOnboardingTemplate: false };

      let intake = mergeTicketIntake(row.intake);
      let updated = false;

      if (intake.suppress_talkback) {
        return { outgoing: null, intakeAfter: intake, sentOnboardingTemplate: false };
      }

      if (settings.onboarding.enabled && intake.step !== 'done') {
        const { intake: next, outgoing: out, advanced } = nextOnboardingStep(settings, intake, inboundText);
        if (!advanced) {
          if (intake.step === 'idle') {
            log.warn({ ticketId: ticket.id }, '[talkback] Onboarding template empty; staying on idle');
          }
          return { outgoing: null, intakeAfter: intake, sentOnboardingTemplate: false };
        }
        intake = next;
        outgoingMsg = out;
        sentTpl = Boolean(out && String(out).trim());
        updated = true;
      }

      if (updated) {
        await q`
          UPDATE tickets SET intake = ${q.json(intake as never)}, updated_at = NOW() WHERE id = ${ticket.id}
        `;
      }
      intakeAfterInner = intake;
      return { outgoing: outgoingMsg, intakeAfter: intakeAfterInner, sentOnboardingTemplate: sentTpl };
    });

    let intakeAfter = intakeFromOnboarding;
    const humanResult = await processHumanRequestInbound({
      ticketId: ticket.id,
      inboundText,
      intake: intakeAfter,
    });
    intakeAfter = humanResult.intake;

    if (humanResult.intakeChanged || humanResult.escalated) {
      const refreshed = await ticketService.getById(ticket.id);
      if (refreshed) {
        io.to(`ticket:${ticket.id}`).emit('ticket:updated', { ticket: refreshed });
        intakeAfter = mergeTicketIntake(refreshed.intake);
      }
    }

    if (humanResult.handled) {
      if (humanResult.outbound?.trim()) {
        await sendAutomatedOutbound({
          io,
          ticketId: ticket.id,
          channelId: channel.id,
          contactPhone,
          text: humanResult.outbound.trim(),
        });
      }
      return;
    }

    // Booking / call-out / Teams requests: suggest on calendar, reply with availability + type clarification
    const sched = await schedulingIntentService.tryHandleInbound({
      io,
      log,
      ticket,
      messageId: messageId ?? null,
      inboundText,
    });
    if (sched.handled) {
      if (sched.customerReply?.trim()) {
        await sendAutomatedOutbound({
          io,
          ticketId: ticket.id,
          channelId: channel.id,
          contactPhone,
          text: sched.customerReply.trim(),
        });
      }
      return;
    }

    const companyName = intakeAfter.answers.company?.trim();
    if (companyName) {
      try {
        const comp = await companyService.upsertByName(companyName);
        await companyService.linkContactToCompany(ticket.contact_id, comp.id);
      } catch (err) {
        log.warn({ err }, '[talkback] company link failed');
      }
    }

    io.to(`ticket:${ticket.id}`).emit('ticket:updated', {
      ticket: { id: ticket.id, intake: intakeAfter },
    });

    const autoReply = outgoing?.trim();
    if (autoReply) {
      await sendAutomatedOutbound({
        io,
        ticketId: ticket.id,
        channelId: channel.id,
        contactPhone,
        text: autoReply,
      });
    }

    const aiEligibleBase =
      intakeAfter.step === 'done' &&
      settings.ai.enabled &&
      !intakeAfter.suppress_talkback &&
      !intakeAfter.awaiting_human_confirm;

    if (aiEligibleBase) {
      if (sentOnboardingTemplate) return;

      const [contactRow] = await sql<Array<{ company_id: string | null }>>`
        SELECT company_id FROM contacts WHERE id = ${ticket.contact_id}
      `;
      const companyId = contactRow?.company_id ?? null;
      if (!companyId) {
        log.info({ ticketId: ticket.id }, '[talkback] skip AI: contact not linked to a company');
        return;
      }

      const [coRow] = await sql<Array<{ ai_autoreply_enabled: boolean }>>`
        SELECT ai_autoreply_enabled FROM companies WHERE id = ${companyId}
      `;
      if (!coRow?.ai_autoreply_enabled) {
        log.info({ ticketId: ticket.id, companyId }, '[talkback] skip AI: company ai_autoreply_enabled is false');
        return;
      }

      const agentRow = await aiAgentService.getDefaultAgentForCompany(companyId);
      const agentKbIds = agentRow ? await aiAgentService.getKnowledgeModuleIds(agentRow.id) : [];
      const useScopedKb = agentKbIds.length > 0;
      const companyKnowledge = useScopedKb
        ? await knowledgeModuleService.buildCompanyKnowledgeMarkdownScoped(companyId, agentKbIds)
        : await knowledgeModuleService.buildCompanyKnowledgeMarkdown(companyId);
      if (!companyKnowledge) {
        log.info(
          { ticketId: ticket.id, companyId, useScopedKb },
          '[talkback] skip AI: no knowledge modules for company (check company enablement and agent KB scope)',
        );
        return;
      }

      const llmReady = await aiAgentService.isRunnableForCompany(companyId);
      if (!llmReady) {
        log.info({ ticketId: ticket.id, companyId }, '[talkback] skip AI: no agent API key and no platform LLM key');
        const offline = channelAiOfflineMessage(settings);
        if (offline) {
          await sendAutomatedOutbound({
            io,
            ticketId: ticket.id,
            channelId: channel.id,
            contactPhone,
            text: offline,
          });
        }
        return;
      }

      const agent = agentRow?.api_key?.trim() ? agentRow : null;

      await runAiReply({
        io,
        log,
        ticketId: ticket.id,
        channelId: channel.id,
        contactPhone,
        settings,
        intake: intakeAfter,
        companyKnowledge,
        agent,
      });
    }
  },
};
