import type { FastifyInstance } from 'fastify';
import { authenticate, requireRole } from '../middleware/auth.js';
import { sql } from '../db.js';
import { ticketService } from '../services/ticketService.js';
import { messagingService } from '../services/messagingService.js';
import { channelService } from '../services/channelService.js';
import { contactService } from '../services/contactService.js';
import { authService } from '../services/authService.js';
import { auditService } from '../services/auditService.js';
import { userCanAccessTicket } from '../services/ticketAccessService.js';
import { isPortalChannelPhone } from '@wa-ticketing/shared';
import { sendSmtpMail } from '../services/emailService.js';
import { emitMessageNew } from '../services/messageEmitService.js';
import {
  uploadTicketObject,
  createSignedUrlForPath,
  normalizeMime,
  enrichMessageWithMediaUrl,
  requireMediaKindFromMime,
  MAX_MEDIA_FILES_PER_REQUEST,
  documentDisplayText,
  sanitizeFileName,
} from '../services/ticketMediaService.js';
import { replySuggestionService } from '../services/replySuggestionService.js';
import { ticketGroupService } from '../services/ticketGroupService.js';
import { assertCapability, canStaffSendOnChannel } from '../services/channelProviderRules.js';

const staffOnly = [authenticate, requireRole('admin', 'user')];

/** WhatsApp send target: group JID when ticket is group-backed, else contact phone. */
function ticketWaTarget(ticket: { wa_group_jid?: string | null }, contactPhone: string): string {
  return ticket.wa_group_jid?.trim() || contactPhone;
}

type StaffMultipartFile = {
  buffer: Buffer;
  mimetype: string;
  filename: string;
};

type StaffMultipartFilePart = {
  type: 'file';
  toBuffer: () => Promise<Buffer>;
  mimetype: string;
  fieldname: string;
  filename?: string;
};

async function parseStaffMultipartFiles(req: {
  parts: () => AsyncIterable<StaffMultipartFilePart | { type: 'field'; fieldname: string; value?: unknown }>;
}): Promise<{ caption: string; files: StaffMultipartFile[] }> {
  let caption = '';
  const files: StaffMultipartFile[] = [];
  for await (const part of req.parts()) {
    if (part.type === 'file') {
      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 filename = part.filename?.trim() || 'document';
      files.push({
        buffer: await part.toBuffer(),
        mimetype: part.mimetype,
        filename,
      });
    } else if (part.fieldname === 'caption' || part.fieldname === 'content') {
      caption = String(part.value ?? '').trim();
    }
  }
  return { caption, files };
}

export async function ticketRoutes(fastify: FastifyInstance) {
  fastify.get('/', { preHandler: authenticate }, async (req, reply) => {
    const q = req.query as Record<string, string>;
    const companyId =
      req.user!.role === 'company_user' ? (req.user!.company_id ?? undefined) : undefined;
    if (req.user!.role === 'company_user' && !companyId) {
      return reply.status(403).send({ error: 'Invalid company user' });
    }

    const companyUserContext =
      req.user!.role === 'company_user' && companyId
        ? { userId: req.user!.sub, companyId }
        : undefined;

    const tickets = await ticketService.list({
      status:     q.status     as 'open' | 'closed' | undefined,
      priority:   q.priority   as 'low' | 'medium' | 'high' | undefined,
      channelId:  q.channel_id,
      assignedTo: q.assigned_to,
      from:       q.from,
      to:         q.to,
      companyUserContext,
      limit:      q.limit  ? Number(q.limit)  : 50,
      offset:     q.offset ? Number(q.offset) : 0,
    });
    return reply.send(tickets);
  });

  /** Staff: create ticket + first WhatsApp outbound (always a new ticket). Direct or group. */
  fastify.post('/', { preHandler: staffOnly }, async (req, reply) => {
    const actorId = req.user!.sub;
    const body = req.body as {
      channel_id?: string;
      contact_id?: string;
      phone_number?: string;
      content?: string;
      mode?: 'direct' | 'group';
      staff_user_ids?: string[];
      group_subject?: string;
    };

    const channelId = body.channel_id?.trim();
    const content = body.content?.trim() ?? '';
    const contactIdIn = body.contact_id?.trim();
    const phoneIn = body.phone_number?.trim();
    const mode = body.mode === 'group' ? 'group' : 'direct';

    if (!channelId) return reply.status(400).send({ error: 'channel_id is required' });
    if (!content) return reply.status(400).send({ error: 'content is required' });

    const hasContact = Boolean(contactIdIn);
    const hasPhone = Boolean(phoneIn);
    if (hasContact === hasPhone) {
      return reply.status(400).send({ error: 'Provide exactly one of contact_id or phone_number' });
    }

    const channel = await channelService.getById(channelId);
    if (!channel) return reply.status(404).send({ error: 'Channel not found' });
    if (channel.kind !== 'whatsapp') {
      return reply.status(400).send({ error: 'Only connected WhatsApp channels are supported' });
    }
    if (!canStaffSendOnChannel(channel)) {
      return reply.status(400).send({ error: 'Channel is not connected' });
    }
    // Reject before any contact/group/ticket writes
    if (mode === 'group') {
      try {
        assertCapability(channel, 'groups');
      } catch (e: unknown) {
        const err = e as { message: string; code?: string };
        return reply.status(400).send({ error: err.message, code: err.code });
      }
    }

    let contact;
    if (contactIdIn) {
      contact = await contactService.getById(contactIdIn);
      if (!contact) return reply.status(404).send({ error: 'Contact not found' });
      if (!contactService.isWhatsAppPhone(contact.phone_number)) {
        return reply.status(400).send({ error: 'Contact is not a WhatsApp phone contact' });
      }
    } else {
      if (!contactService.isWhatsAppPhone(phoneIn!)) {
        return reply.status(400).send({ error: 'phone_number must be a WhatsApp number' });
      }
      const normalized = contactService.normalize(phoneIn!);
      if (normalized.replace(/\D/g, '').length < 8) {
        return reply.status(400).send({ error: 'phone_number looks invalid' });
      }
      contact = await contactService.findOrCreate(phoneIn!);
    }

    let waGroupJid: string | null = null;
    let waGroupSubject: string | null = null;
    let staffIds: string[] = [];

    if (mode === 'group') {
      staffIds = Array.isArray(body.staff_user_ids)
        ? [...new Set(body.staff_user_ids.map(String).filter(Boolean))]
        : [];
      if (staffIds.length < 1 || staffIds.length > 3) {
        return reply.status(400).send({ error: 'Select 1 to 3 staff members with WhatsApp phones' });
      }

      const staffPhones: string[] = [];
      for (const uid of staffIds) {
        const u = await authService.getUser(uid);
        if (!u || (u.role !== 'admin' && u.role !== 'user') || !u.is_active) {
          return reply.status(400).send({ error: `Invalid staff user: ${uid}` });
        }
        if (!u.phone?.trim() || !contactService.isWhatsAppPhone(u.phone)) {
          return reply.status(400).send({ error: `Staff ${u.email} has no WhatsApp phone on their profile` });
        }
        staffPhones.push(contactService.normalize(u.phone));
      }

      const clientLabel =
        contact.display_name?.trim() ||
        contact.phone_number;
      waGroupSubject =
        (body.group_subject?.trim() || `Silicon Support — ${clientLabel}`).slice(0, 100);

      const participantPhones = [
        contactService.normalize(contact.phone_number),
        ...staffPhones,
      ];
      // Dedupe by digits
      const seen = new Set<string>();
      const uniquePhones = participantPhones.filter(p => {
        const d = p.replace(/\D/g, '');
        if (seen.has(d)) return false;
        seen.add(d);
        return true;
      });

      try {
        const created = await channelService.createWhatsAppGroup(channelId, waGroupSubject, uniquePhones);
        waGroupJid = created.groupJid;
        waGroupSubject = created.subject;
      } catch (err) {
        return reply.status(502).send({ error: `Could not create WhatsApp group: ${String(err)}` });
      }
    }

    const ticket = await ticketService.createTicket(contact.id, channelId, { step: 'done', answers: {} }, {
      assignedTo: actorId,
      actorId,
      waGroupJid,
      waGroupSubject,
      waGroupParticipantUserIds: staffIds,
    });

    const hydrated = await ticketService.getById(ticket.id);
    const message = await messagingService.appendMessage(ticket.id, 'outbound', content, null, actorId);
    const sendTo = ticketWaTarget(hydrated ?? ticket, contact.phone_number);

    try {
      const waMessageId = await channelService.sendMessage(channelId, sendTo, content);
      if (waMessageId) {
        await sql`UPDATE messages SET wa_message_id = ${waMessageId} WHERE id = ${message.id}`;
        message.wa_message_id = waMessageId;
      }
      await auditService.log(actorId, 'user', 'message.sent', 'message', message.id, {
        channelId,
        ticketId: ticket.id,
        staff_initiated: true,
        mode,
      });
      fastify.io.to(`ticket:${ticket.id}`).emit('message:sent', { messageId: message.id, status: 'ok' });
    } catch (err) {
      await auditService.log(actorId, 'user', 'message.send_failed', 'message', message.id, {
        error: String(err),
        staff_initiated: true,
        mode,
      });
      fastify.io.to(`ticket:${ticket.id}`).emit('message:sent', {
        messageId: message.id,
        status: 'error',
        error: String(err),
      });
    }

    const enrichedMessage = await enrichMessageWithMediaUrl(message);
    fastify.io.emit('ticket:created', { ticket: hydrated ?? ticket });
    await emitMessageNew(fastify.io, ticket.id, enrichedMessage);

    return reply.status(201).send({ ticket: hydrated ?? ticket, message: enrichedMessage });
  });

  fastify.get('/:id', { preHandler: authenticate }, async (req, reply) => {
    const { id } = req.params as { id: string };
    if (!(await userCanAccessTicket(req.user!, id))) {
      return reply.status(403).send({ error: 'Forbidden' });
    }
    const ticket = await ticketService.getById(id);
    if (!ticket) return reply.status(404).send({ error: 'Ticket not found' });
    const messages = await messagingService.getMessages(id);
    const enriched = await Promise.all(messages.map(m => enrichMessageWithMediaUrl(m)));
    return reply.send({ ...ticket, messages: enriched });
  });

  // WhatsApp group ticket — live members + add/remove staff
  fastify.get('/:id/group', { preHandler: staffOnly }, async (req, reply) => {
    const { id } = req.params as { id: string };
    if (!(await userCanAccessTicket(req.user!, id))) {
      return reply.status(403).send({ error: 'Forbidden' });
    }
    try {
      return reply.send(await ticketGroupService.getGroupView(id));
    } catch (err) {
      const status = (err as { statusCode?: number }).statusCode ?? 500;
      return reply.status(status).send({ error: err instanceof Error ? err.message : String(err) });
    }
  });

  fastify.post('/:id/group/participants', { preHandler: staffOnly }, async (req, reply) => {
    const { id } = req.params as { id: string };
    if (!(await userCanAccessTicket(req.user!, id))) {
      return reply.status(403).send({ error: 'Forbidden' });
    }
    const { user_id: userId } = (req.body ?? {}) as { user_id?: string };
    if (!userId?.trim()) return reply.status(400).send({ error: 'user_id required' });
    try {
      return reply.send(await ticketGroupService.addStaff(id, userId.trim()));
    } catch (err) {
      const status = (err as { statusCode?: number }).statusCode ?? 500;
      return reply.status(status).send({ error: err instanceof Error ? err.message : String(err) });
    }
  });

  fastify.delete('/:id/group/participants', { preHandler: staffOnly }, async (req, reply) => {
    const { id } = req.params as { id: string };
    if (!(await userCanAccessTicket(req.user!, id))) {
      return reply.status(403).send({ error: 'Forbidden' });
    }
    const { user_id: userId } = (req.body ?? {}) as { user_id?: string };
    if (!userId?.trim()) return reply.status(400).send({ error: 'user_id required' });
    try {
      return reply.send(await ticketGroupService.removeStaff(id, userId.trim()));
    } catch (err) {
      const status = (err as { statusCode?: number }).statusCode ?? 500;
      return reply.status(status).send({ error: err instanceof Error ? err.message : String(err) });
    }
  });

  fastify.patch('/:id', { preHandler: staffOnly }, async (req, reply) => {
    const { id } = req.params as { id: string };
    if (!(await userCanAccessTicket(req.user!, id))) {
      return reply.status(403).send({ error: 'Forbidden' });
    }
    const body = req.body as {
      status?: 'closed';
      priority?: 'low' | 'medium' | 'high';
      due_date?: string | null;
      assigned_to?: string | null;
      needs_escalation?: boolean;
    };
    const actorId = req.user!.sub;

    let ticket;
    if (body.status === 'closed')            ticket = await ticketService.closeTicket(id, actorId, fastify.io);
    if (body.priority !== undefined)         ticket = await ticketService.setPriority(id, body.priority, actorId);
    if ('due_date' in body)                  ticket = await ticketService.setDueDate(id, body.due_date ?? null, actorId);
    if ('assigned_to' in body)               ticket = await ticketService.assignTo(id, body.assigned_to ?? null, actorId);
    if (body.needs_escalation !== undefined)
      ticket = await ticketService.setNeedsEscalation(id, body.needs_escalation, actorId);

    ticket = ticket ?? await ticketService.getById(id);
    if (!ticket) return reply.status(404).send({ error: 'Ticket not found' });

    fastify.io.to(`ticket:${id}`).emit('ticket:updated', { ticket });
    return reply.send(ticket);
  });

  fastify.post('/:id/reply-suggestions', { preHandler: staffOnly }, async (req, reply) => {
    const { id } = req.params as { id: string };
    if (!(await userCanAccessTicket(req.user!, id))) {
      return reply.status(403).send({ error: 'Forbidden' });
    }
    const result = await replySuggestionService.generate(id);
    return reply.send(result);
  });

  fastify.post('/:id/messages', { preHandler: staffOnly }, async (req, reply) => {
    const { id } = req.params as { id: string };
    const actorId = req.user!.sub;

    if (!(await userCanAccessTicket(req.user!, id))) {
      return reply.status(403).send({ error: 'Forbidden' });
    }

    const ticket = await ticketService.getById(id);
    if (!ticket) return reply.status(404).send({ error: 'Ticket not found' });
    if (ticket.status === 'closed') return reply.status(400).send({ error: 'Ticket is closed' });

    const channel = await channelService.getById(ticket.channel_id);

    if (req.isMultipart()) {
      if (channel?.kind === 'email') {
        return reply.status(400).send({ error: 'Email tickets do not support media attachments' });
      }

      let caption: string;
      let files: StaffMultipartFile[];
      try {
        const parsed = await parseStaffMultipartFiles(req);
        caption = parsed.caption;
        files = parsed.files;
      } catch (e: unknown) {
        const err = e as { statusCode?: number; message?: string };
        return reply.status(err.statusCode ?? 400).send({ error: err.message ?? 'Invalid multipart' });
      }

      if (!caption && files.length === 0) {
        return reply.status(400).send({ error: 'content or at least one file is required' });
      }

      const isPortal = channel && isPortalChannelPhone(channel.phone_number);
      const outbound: Awaited<ReturnType<typeof messagingService.appendMessage>>[] = [];

      const pushPortalOrWa = async (
        msg: Awaited<ReturnType<typeof messagingService.appendMessage>>,
        waOk: boolean,
      ) => {
        outbound.push(msg);
        await auditService.log(actorId, 'user', 'message.sent', 'message', msg.id, {
          channelId: ticket.channel_id,
          ticketId: id,
          ...(isPortal ? { source: 'portal' as const } : {}),
        });
        fastify.io.to(`ticket:${id}`).emit('message:sent', {
          messageId: msg.id,
          status: waOk ? 'ok' : 'error',
        });
        await emitMessageNew(fastify.io, id, msg);
      };

      if (caption) {
        if (isPortal) {
          const message = await messagingService.appendMessage(
            id,
            'outbound',
            caption,
            null,
            actorId,
            'portal',
          );
          await pushPortalOrWa(message, true);
        } else {
          const message = await messagingService.appendMessage(id, 'outbound', caption, null, actorId);
          try {
            if (!canStaffSendOnChannel(channel)) throw new Error('Channel not connected');
            const [contact] = await sql<[{ phone_number: string }]>`
              SELECT phone_number FROM contacts WHERE id = ${ticket.contact_id}
            `;
            if (!contact) throw new Error('Contact not found');
            const waMessageId = await channelService.sendMessage(
              ticket.channel_id,
              ticketWaTarget(ticket, contact.phone_number),
              caption,
            );
            if (waMessageId) {
              await sql`UPDATE messages SET wa_message_id = ${waMessageId} WHERE id = ${message.id}`;
              message.wa_message_id = waMessageId;
            }
            await pushPortalOrWa(message, true);
          } catch (err) {
            await auditService.log(actorId, 'user', 'message.send_failed', 'message', message.id, {
              error: String(err),
            });
            fastify.io.to(`ticket:${id}`).emit('message:sent', {
              messageId: message.id,
              status: 'error',
              error: String(err),
            });
            await emitMessageNew(fastify.io, id, message);
            outbound.push(message);
          }
        }
      }

      for (const f of files) {
        const kind = requireMediaKindFromMime(f.mimetype);
        const { path, sizeBytes } = await uploadTicketObject({
          ticketId: 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, caption || undefined);
        const message = await messagingService.appendMessage(
          id,
          'outbound',
          display,
          null,
          actorId,
          isPortal ? 'portal' : 'whatsapp',
          {
            messageType: kind,
            mediaPath: path,
            mediaMime: normalizeMime(f.mimetype),
            mediaSizeBytes: sizeBytes,
            mediaDurationSec: null,
            mediaCaption: kind === 'document' ? docName : null,
          },
        );

        if (isPortal) {
          await pushPortalOrWa(message, true);
          continue;
        }

        try {
          if (!canStaffSendOnChannel(channel)) throw new Error('Channel not connected');
          const [contact] = await sql<[{ phone_number: string }]>`
            SELECT phone_number FROM contacts WHERE id = ${ticket.contact_id}
          `;
          if (!contact) throw new Error('Contact not found');
          // Africa's Talking fetches media from a hosted URL; Baileys keeps using base64.
          const mediaUrl = channel.provider === 'africas_talking' ? await createSignedUrlForPath(path) : null;
          if (channel.provider === 'africas_talking' && !mediaUrl) {
            throw new Error('Could not create a signed media URL for the provider');
          }
          const waMessageId = await channelService.sendMediaMessage(
            ticket.channel_id,
            ticketWaTarget(ticket, contact.phone_number),
            {
              kind,
              mediaUrl,
              mediaBase64: f.buffer.toString('base64'),
              mediaMime: normalizeMime(f.mimetype),
              caption: kind === 'image' ? caption || undefined : undefined,
              fileName: kind === 'document' ? docName : undefined,
            },
          );
          if (waMessageId) {
            await sql`UPDATE messages SET wa_message_id = ${waMessageId} WHERE id = ${message.id}`;
            message.wa_message_id = waMessageId;
          }
          await pushPortalOrWa(message, true);
        } catch (err) {
          await auditService.log(actorId, 'user', 'message.send_failed', 'message', message.id, {
            error: String(err),
          });
          fastify.io.to(`ticket:${id}`).emit('message:sent', {
            messageId: message.id,
            status: 'error',
            error: String(err),
          });
          await emitMessageNew(fastify.io, id, message);
          outbound.push(message);
        }
      }

      const enriched = await Promise.all(outbound.map(m => enrichMessageWithMediaUrl(m)));
      return reply.status(201).send({ messages: enriched });
    }

    const { content } = req.body as { content: string };
    if (!content?.trim()) return reply.status(400).send({ error: 'content is required' });

    if (channel && isPortalChannelPhone(channel.phone_number)) {
      const message = await messagingService.appendMessage(
        id,
        'outbound',
        content.trim(),
        null,
        actorId,
        'portal',
      );
      await auditService.log(actorId, 'user', 'message.sent', 'message', message.id, {
        channelId: ticket.channel_id,
        ticketId: id,
        source: 'portal',
      });
      fastify.io.to(`ticket:${id}`).emit('message:sent', { messageId: message.id, status: 'ok' });
      await emitMessageNew(fastify.io, id, message);
      return reply.status(201).send(await enrichMessageWithMediaUrl(message));
    }

    if (channel?.kind === 'email') {
      const [contact] = await sql<[{ phone_number: string }]>`
        SELECT phone_number FROM contacts WHERE id = ${ticket.contact_id}
      `;
      if (!contact?.phone_number.startsWith('email:') || !contact.phone_number.includes('@')) {
        return reply.status(400).send({ error: 'Invalid email contact' });
      }
      const to = contact.phone_number.slice(6);
      const sent = await sendSmtpMail({
        to,
        subject: `Support: ticket ${id.slice(0, 8)}`,
        text: content.trim(),
      });
      if (!sent.ok) {
        return reply.status(503).send({ error: sent.error ?? 'Email send failed (configure SMTP on API)' });
      }
      const message = await messagingService.appendMessage(id, 'outbound', content.trim(), null, actorId, 'email');
      await auditService.log(actorId, 'user', 'message.sent', 'message', message.id, {
        channelId: ticket.channel_id,
        ticketId: id,
        source: 'email',
      });
      fastify.io.to(`ticket:${id}`).emit('message:sent', { messageId: message.id, status: 'ok' });
      await emitMessageNew(fastify.io, id, message);
      return reply.status(201).send(await enrichMessageWithMediaUrl(message));
    }

    const message = await messagingService.appendMessage(id, 'outbound', content.trim(), null, actorId);

    try {
      if (!canStaffSendOnChannel(channel)) {
        throw new Error('Channel not connected');
      }

      const [contact] = await sql<[{ phone_number: string }]>`
        SELECT phone_number FROM contacts WHERE id = ${ticket.contact_id}
      `;
      if (!contact) throw new Error('Contact not found');

      const waMessageId = await channelService.sendMessage(
        ticket.channel_id,
        ticketWaTarget(ticket, contact.phone_number),
        content.trim(),
      );

      if (waMessageId) {
        await sql`UPDATE messages SET wa_message_id = ${waMessageId} WHERE id = ${message.id}`;
        message.wa_message_id = waMessageId;
      }

      await auditService.log(actorId, 'user', 'message.sent', 'message', message.id, {
        channelId: ticket.channel_id,
        ticketId: id,
      });

      fastify.io.to(`ticket:${id}`).emit('message:sent', { messageId: message.id, status: 'ok' });
      await emitMessageNew(fastify.io, id, message);
    } catch (err) {
      await auditService.log(actorId, 'user', 'message.send_failed', 'message', message.id, {
        error: String(err),
      });
      fastify.io.to(`ticket:${id}`).emit('message:sent', { messageId: message.id, status: 'error', error: String(err) });
    }

    return reply.status(201).send(await enrichMessageWithMediaUrl(message));
  });
}
