import type { FastifyBaseLogger } from 'fastify';
import type { Server as SocketServer } from 'socket.io';
import type { Message } from '@wa-ticketing/shared';
import { defaultTicketIntake } from '@wa-ticketing/shared';
import { contactService } from './contactService.js';
import { companyService } from './companyService.js';
import { ticketService } from './ticketService.js';
import { messagingService } from './messagingService.js';
import { auditService } from './auditService.js';
import { channelService } from './channelService.js';
import { talkbackService } from './talkbackService.js';
import { notifyInboundAlerts } from './channelAlertService.js';
import { emitMessageNew } from './messageEmitService.js';
import {
  uploadTicketObject,
  normalizeMime,
  requireMediaKindFromMime,
  MAX_MEDIA_FILES_PER_REQUEST,
  documentDisplayText,
  sanitizeFileName,
} from './ticketMediaService.js';

function mediaBatchSummary(kinds: Array<'image' | 'voice_note' | 'document'>): string {
  const img = kinds.filter(k => k === 'image').length;
  const vn = kinds.filter(k => k === 'voice_note').length;
  const doc = kinds.filter(k => k === 'document').length;
  const p: string[] = [];
  if (img) p.push(`${img} image${img > 1 ? 's' : ''}`);
  if (vn) p.push(`${vn} voice note${vn > 1 ? 's' : ''}`);
  if (doc) p.push(`${doc} file${doc > 1 ? 's' : ''}`);
  return p.join(', ');
}

function talkbackSummaryForBatch(textTrim: string, kinds: Array<'image' | 'voice_note' | 'document'>): string {
  const media = mediaBatchSummary(kinds);
  if (textTrim && media) return `${textTrim}\n[${media}]`;
  if (textTrim) return textTrim;
  return `[${media}]`;
}

type PortalChannel = NonNullable<Awaited<ReturnType<typeof channelService.getById>>>;

/** Resolve target ticket: explicit id, or open ticket, or always create when forceNew. */
async function resolvePortalTicket(opts: {
  io: SocketServer;
  contactId: string;
  portalChannelId: string;
  portalChannel: PortalChannel;
  ticketId?: string;
  forceNew?: boolean;
}) {
  const { io, contactId, portalChannelId, portalChannel, ticketId, forceNew } = opts;

  if (ticketId) {
    const ticket = await ticketService.getById(ticketId);
    if (!ticket) throw Object.assign(new Error('Ticket not found'), { statusCode: 404 });
    if (ticket.contact_id !== contactId || ticket.channel_id !== portalChannelId) {
      throw Object.assign(new Error('Forbidden'), { statusCode: 403 });
    }
    if (ticket.status !== 'open') {
      throw Object.assign(new Error('Ticket is closed'), { statusCode: 400 });
    }
    return ticket;
  }

  if (!forceNew) {
    const open = await ticketService.findOpenTicket(contactId, portalChannelId);
    if (open) return open;
  }

  const priorCount = await ticketService.countForContactChannel(contactId, portalChannelId);
  const intake =
    priorCount === 0
      ? ticketService.buildInitialIntake(portalChannel.settings!, true)
      : defaultTicketIntake;
  const ticket = await ticketService.createTicket(contactId, portalChannelId, intake);
  const hydrated = await ticketService.getById(ticket.id);
  io.emit('ticket:created', { ticket: hydrated ?? ticket });
  return hydrated ?? ticket;
}

export const portalService = {
  /**
   * Company user sends a message into support: append inbound (portal) on open ticket or create one.
   * Runs the same onboarding / AI talkback as WhatsApp (settings inherited from primary WA channel on web chat).
   */
  async postInboundFromCompanyUser(opts: {
    io: SocketServer;
    log: FastifyBaseLogger;
    userId: string;
    companyId: string;
    content: string;
    ticketId?: string;
    /** When true and no ticket_id, always opens a new ticket (never reuse an open one). */
    forceNew?: boolean;
  }) {
    const { io, log, userId, companyId, content, ticketId, forceNew } = opts;
    const text = content.trim();
    if (!text) throw Object.assign(new Error('content is required'), { statusCode: 400 });

    const portalChannelId = await companyService.getPortalChannelId();
    const portalChannel = await channelService.getById(portalChannelId);
    if (!portalChannel?.settings) {
      throw Object.assign(new Error('Web chat channel not configured'), { statusCode: 500 });
    }

    const contact = await contactService.ensurePortalUser(userId, companyId);

    const ticket = await resolvePortalTicket({
      io,
      contactId: contact.id,
      portalChannelId,
      portalChannel,
      ticketId,
      forceNew,
    });

    const message = await messagingService.appendMessage(
      ticket!.id,
      'inbound',
      text,
      null,
      undefined,
      'portal',
    );

    await auditService.log(userId, 'user', 'portal.message', 'message', message.id, {
      ticketId: ticket!.id,
    });

    await emitMessageNew(io, ticket!.id, message);

    if (portalChannel.settings) {
      notifyInboundAlerts({
        ticketId: ticket!.id,
        channelId: portalChannelId,
        channelName: portalChannel.name,
        fromLabel: contact.phone_number,
        preview: text,
        settings: portalChannel.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 }, '[portal] channel alerts failed'));
    }

    const ticketFresh = await ticketService.getById(ticket!.id);
    if (ticketFresh) {
      talkbackService
        .handleInbound({
          io,
          log,
          ticket: ticketFresh,
          channel: portalChannel,
          contactPhone: contact.phone_number,
          inboundText: text,
          messageId: message.id,
        })
        .catch(err => {
          log.error({ err, ticketId: ticket!.id }, '[portal] talkback failed');
        });
    }

    return { ticket: ticket!, message };
  },

  /**
   * Company user sends one or more media parts (images / voice) with optional text.
   * One messages row per file; optional leading text row when content is non-empty.
   * Single talkback + single alert preview after all rows are persisted.
   */
  async postInboundFromCompanyUserWithFiles(opts: {
    io: SocketServer;
    log: FastifyBaseLogger;
    userId: string;
    companyId: string;
    content: string;
    ticketId?: string;
    forceNew?: boolean;
    files: Array<{ buffer: Buffer; mimetype: string; filename: string }>;
  }): Promise<{ ticket: NonNullable<Awaited<ReturnType<typeof ticketService.getById>>>; messages: Message[] }> {
    const { io, log, userId, companyId, content, ticketId, forceNew, files } = opts;
    const text = content.trim();

    if (!text && files.length === 0) {
      throw Object.assign(new Error('content or at least one file is required'), { statusCode: 400 });
    }
    if (files.length > MAX_MEDIA_FILES_PER_REQUEST) {
      throw Object.assign(new Error(`At most ${MAX_MEDIA_FILES_PER_REQUEST} files per request`), {
        statusCode: 400,
      });
    }

    const portalChannelId = await companyService.getPortalChannelId();
    const portalChannel = await channelService.getById(portalChannelId);
    if (!portalChannel?.settings) {
      throw Object.assign(new Error('Web chat channel not configured'), { statusCode: 500 });
    }

    const contact = await contactService.ensurePortalUser(userId, companyId);

    const ticket = await resolvePortalTicket({
      io,
      contactId: contact.id,
      portalChannelId,
      portalChannel,
      ticketId,
      forceNew,
    });

    const messages: Message[] = [];
    const kinds: Array<'image' | 'voice_note' | 'document'> = [];

    if (text) {
      const textMsg = await messagingService.appendMessage(
        ticket!.id,
        'inbound',
        text,
        null,
        undefined,
        'portal',
      );
      messages.push(textMsg);
      await auditService.log(userId, 'user', 'portal.message', 'message', textMsg.id, {
        ticketId: ticket!.id,
      });
      await emitMessageNew(io, ticket!.id, textMsg);
    }

    for (const f of files) {
      const kind = requireMediaKindFromMime(f.mimetype);
      kinds.push(kind);
      const { path, sizeBytes } = await uploadTicketObject({
        ticketId: ticket!.id,
        buffer: f.buffer,
        mime: f.mimetype,
        messageType: kind,
      });
      const docName = sanitizeFileName(f.filename || 'document');
      const display =
        kind === 'image'
          ? '[Image]'
          : kind === 'voice_note'
            ? '[Voice note]'
            : documentDisplayText(docName);
      const msg = await messagingService.appendMessage(
        ticket!.id,
        'inbound',
        display,
        null,
        undefined,
        'portal',
        {
          messageType: kind,
          mediaPath: path,
          mediaMime: normalizeMime(f.mimetype),
          mediaSizeBytes: sizeBytes,
          mediaDurationSec: null,
          mediaCaption: kind === 'document' ? docName : null,
        },
      );
      messages.push(msg);
      await auditService.log(userId, 'user', 'portal.message', 'message', msg.id, {
        ticketId: ticket!.id,
      });
      await emitMessageNew(io, ticket!.id, msg);
    }

    const preview =
      text ||
      (kinds.length ? `${mediaBatchSummary(kinds)}` : '') ||
      '[Media]';

    if (portalChannel.settings) {
      notifyInboundAlerts({
        ticketId: ticket!.id,
        channelId: portalChannelId,
        channelName: portalChannel.name,
        fromLabel: contact.phone_number,
        preview: preview.slice(0, 2000),
        settings: portalChannel.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 }, '[portal] channel alerts failed'));
    }

    const ticketFresh = await ticketService.getById(ticket!.id);
    if (ticketFresh && (text || kinds.length)) {
      const inboundText = talkbackSummaryForBatch(text, kinds);
      talkbackService
        .handleInbound({
          io,
          log,
          ticket: ticketFresh,
          channel: portalChannel,
          contactPhone: contact.phone_number,
          inboundText,
          messageId: messages[0]?.id ?? null,
        })
        .catch(err => {
          log.error({ err, ticketId: ticket!.id }, '[portal] talkback failed');
        });
    }

    const t = await ticketService.getById(ticket!.id);
    if (!t) throw Object.assign(new Error('Ticket not found'), { statusCode: 404 });
    return { ticket: t, messages };
  },
};
