/**
 * Provider-neutral inbound WhatsApp processing.
 * Called by the Baileys worker route (`POST /internal/messages`) and by public
 * provider webhooks, so ticket, media, alert and talkback behavior stays identical.
 */
import type { Server as SocketServer } from 'socket.io';
import type { FastifyBaseLogger } from 'fastify';
import { contactService } from './contactService.js';
import { ticketService } from './ticketService.js';
import { messagingService } from './messagingService.js';
import { channelService } from './channelService.js';
import { auditService } from './auditService.js';
import { talkbackService } from './talkbackService.js';
import { waSignupTalkbackService } from './waSignupTalkbackService.js';
import { notifyInboundAlerts } from './channelAlertService.js';
import {
  decodeBase64ToBuffer,
  uploadTicketObject,
  validateInboundMime,
  normalizeMime,
  documentDisplayText,
  sanitizeFileName,
  transcribeTicketVoiceNote,
} from './ticketMediaService.js';
import { emitMessageNew } from './messageEmitService.js';
import { shouldReplyInWhatsAppGroup } from './groupTalkbackGate.js';

export type NormalizedWhatsAppInbound = {
  channelId: string;
  phone: string;
  waId?: string;
  text?: string;
  messageType?: 'text' | 'image' | 'voice_note' | 'document';
  mediaBase64?: string;
  mediaMime?: string;
  mediaDurationSec?: number;
  mediaFileName?: string;
  /** WhatsApp group JID when message is from a group */
  groupJid?: string;
  /** Digits from @mentions in a group message */
  mentionedPhones?: string[];
  /** Channel account phone + LID digits (from worker sock.user) */
  channelMentionIds?: string[];
};

export type InboundResult =
  | { ok: true; duplicate: true }
  | { ok: true; ignored: true; reason: 'no_group_ticket' }
  | { ok: true; ticketId: string; messageId: string };

export type InboundContext = { io: SocketServer; log: FastifyBaseLogger };

/** Raised when the resolved channel row disappeared; callers map it to 404. */
export class InboundChannelNotFoundError extends Error {
  readonly statusCode = 404;

  constructor() {
    super('Channel not found');
    this.name = 'InboundChannelNotFoundError';
  }
}

export async function processWhatsAppInbound(
  input: NormalizedWhatsAppInbound,
  context: InboundContext,
): Promise<InboundResult> {
  const {
    channelId,
    phone,
    waId,
    text,
    messageType,
    mediaBase64,
    mediaMime,
    mediaDurationSec,
    mediaFileName,
    groupJid,
    mentionedPhones,
    channelMentionIds,
  } = input;
  const { io, log } = context;

  const isImage = messageType === 'image';
  const isVoice = messageType === 'voice_note';
  const isDocument = messageType === 'document';
  const isMedia = isImage || isVoice || isDocument;

  // Deduplication check
  if (waId && (await messagingService.isDuplicate(waId))) {
    return { ok: true, duplicate: true };
  }

  const channel = await channelService.getById(channelId);
  if (!channel) throw new InboundChannelNotFoundError();

  // Group messages: only attach to an existing group ticket — never invent a 1:1 ticket
  if (groupJid) {
    const groupTicket = await ticketService.findOpenByGroupJid(channelId, groupJid);
    if (!groupTicket) {
      console.warn(`[internal/messages] No open group ticket for ${groupJid} on channel ${channelId}`);
      return { ok: true, ignored: true, reason: 'no_group_ticket' };
    }

    const contact = await contactService.findOrCreate(phone);
    let displayText: string;
    let message;

    if (isMedia) {
      const mt = isImage ? 'image' : isVoice ? 'voice_note' : 'document';
      const buf = decodeBase64ToBuffer(mediaBase64!);
      validateInboundMime(mediaMime!, mt);
      const { path, sizeBytes } = await uploadTicketObject({
        ticketId: groupTicket.id,
        buffer: buf,
        mime: mediaMime!,
        messageType: mt,
      });
      const caption = isImage ? (text?.trim() || null) : null;
      const docName = sanitizeFileName(mediaFileName?.trim() || 'document');
      displayText = isImage
        ? text?.trim() || '[Image]'
        : isVoice
          ? text?.trim() || '[Voice note]'
          : documentDisplayText(docName, text);

      message = await messagingService.appendMessage(
        groupTicket.id,
        'inbound',
        displayText,
        waId ?? null,
        undefined,
        'whatsapp',
        {
          messageType: mt,
          mediaPath: path,
          mediaMime: normalizeMime(mediaMime!),
          mediaSizeBytes: sizeBytes,
          mediaDurationSec: mediaDurationSec ?? null,
          mediaCaption: isDocument ? docName : caption,
        },
        contactService.normalize(phone),
      );
      if (isVoice && message.media_path) {
        const transcript = await transcribeTicketVoiceNote(message.media_path);
        if (transcript) {
          const updated = await messagingService.updateContent(message.id, transcript);
          if (updated) message = updated;
        }
      }
    } else {
      displayText = text!.trim();
      message = await messagingService.appendMessage(
        groupTicket.id,
        'inbound',
        displayText,
        waId ?? null,
        undefined,
        'whatsapp',
        null,
        contactService.normalize(phone),
      );
    }

    await emitMessageNew(io, groupTicket.id, message);
    await auditService.log(null, 'system', 'message.received', 'message', message.id, {
      channelId,
      ticketId: groupTicket.id,
      groupJid,
      fromPhone: phone,
    });

    // Group AI: @mention, clear question, or follow-up to channel's last question
    const talkText =
      isVoice && message.content && message.content !== '[Voice note]'
        ? message.content
        : displayText;
    const priorOutbound = await messagingService.getLastOutboundBeforeSequence(
      groupTicket.id,
      message.sequence,
    );
    if (
      shouldReplyInWhatsAppGroup({
        text: talkText,
        channelPhone: channel.phone_number,
        channelName: channel.name,
        mentionedPhones: mentionedPhones ?? [],
        channelMentionIds: channelMentionIds ?? [],
        priorOutbound: priorOutbound ? { content: priorOutbound.content } : null,
      })
    ) {
      const ticketFresh = await ticketService.getById(groupTicket.id);
      if (ticketFresh) {
        talkbackService
          .handleInbound({
            io,
            log,
            ticket: ticketFresh,
            channel,
            contactPhone: contact.phone_number,
            inboundText: talkText,
            messageId: message.id,
          })
          .catch(err => {
            log.error({ err, ticketId: groupTicket.id }, '[internal] group talkback failed');
          });
      }
    } else {
      log.info(
        { ticketId: groupTicket.id },
        '[internal] skip group talkback (not @mentioned, not a clear question, not a follow-up to channel question)',
      );
    }

    return { ok: true, ticketId: groupTicket.id, messageId: message.id };
  }

  const contact = await contactService.findOrCreate(phone);

  const priorCount = await ticketService.countForContactChannel(contact.id, channelId);

  let ticket = await ticketService.findOpenTicket(contact.id, channelId);
  let isNew = false;

  if (!ticket) {
    const intake = ticketService.buildInitialIntake(channel.settings!, priorCount === 0);
    ticket = await ticketService.createTicket(contact.id, channelId, intake);
    isNew = true;
  }

  let displayText: string;
  let message;

  if (isMedia) {
    const mt = isImage ? 'image' : isVoice ? 'voice_note' : 'document';
    const buf = decodeBase64ToBuffer(mediaBase64!);
    validateInboundMime(mediaMime!, mt);
    const { path, sizeBytes } = await uploadTicketObject({
      ticketId: ticket.id,
      buffer: buf,
      mime: mediaMime!,
      messageType: mt,
    });
    const caption = isImage ? (text?.trim() || null) : null;
    const docName = sanitizeFileName(mediaFileName?.trim() || 'document');
    displayText = isImage
      ? text?.trim() || '[Image]'
      : isVoice
        ? text?.trim() || '[Voice note]'
        : documentDisplayText(docName, text);

    message = await messagingService.appendMessage(
      ticket.id,
      'inbound',
      displayText,
      waId ?? null,
      undefined,
      'whatsapp',
      {
        messageType: mt,
        mediaPath: path,
        mediaMime: normalizeMime(mediaMime!),
        mediaSizeBytes: sizeBytes,
        mediaDurationSec: mediaDurationSec ?? null,
        mediaCaption: isDocument ? docName : caption,
      },
    );
  } else {
    displayText = text!.trim();
    message = await messagingService.appendMessage(ticket.id, 'inbound', displayText, waId ?? null);
  }

  // Voice notes: Whisper transcript for staff UI, alerts, and talkback (not just "[Voice note]")
  if (isVoice && message.media_path) {
    const transcript = await transcribeTicketVoiceNote(message.media_path);
    if (transcript) {
      displayText = transcript;
      const updated = await messagingService.updateContent(message.id, transcript);
      if (updated) message = updated;
    }
  }

  await auditService.log(null, 'system', 'message.received', 'message', message.id, {
    channelId,
    ticketId: ticket.id,
    phone,
  });

  if (isNew) {
    io.emit('ticket:created', { ticket });
  }
  await emitMessageNew(io, ticket.id, message);

  if (channel.settings) {
    notifyInboundAlerts({
      ticketId: ticket.id,
      channelId,
      channelName: channel.name,
      fromLabel: phone,
      preview: displayText,
      settings: channel.settings,
    })
      .then(async (res: { flaggedEscalation: boolean }) => {
        if (res.flaggedEscalation) {
          const t2 = await ticketService.getById(ticket.id);
          if (t2) io.to(`ticket:${ticket.id}`).emit('ticket:updated', { ticket: t2 });
        }
      })
      .catch((err: unknown) => log.warn({ err }, '[internal] channel alerts failed'));
  }

  const channelForTalkback = await channelService.getById(channelId);
  if (channelForTalkback) {
    const ticketFresh = await ticketService.getById(ticket.id);
    const contactFresh = await contactService.getById(contact.id);
    const runSignup =
      contactFresh &&
      !contactFresh.company_id &&
      contactService.isWhatsAppPhone(contactFresh.phone_number);

    if (runSignup && ticketFresh) {
      waSignupTalkbackService
        .handleInbound({
          io,
          log,
          ticket: ticketFresh,
          channel: channelForTalkback,
          contactPhone: contactFresh.phone_number,
          inboundText: displayText,
        })
        .catch(err => {
          log.error({ err, ticketId: ticket.id }, '[internal] wa signup failed');
        });
    } else {
      talkbackService
        .handleInbound({
          io,
          log,
          ticket,
          channel: channelForTalkback,
          contactPhone: contact.phone_number,
          inboundText: displayText,
          messageId: message.id,
        })
        .catch(err => {
          log.error({ err, ticketId: ticket.id }, '[internal] talkback failed');
        });
    }
  } else {
    log.error({ channelId }, '[internal] channel row missing after persist; skip talkback');
  }

  return { ok: true, ticketId: ticket.id, messageId: message.id };
}
