import type { Server as SocketServer } from 'socket.io';
import { sql } from '../db.js';
import type {
  ChannelSettings,
  PortalParticipant,
  Ticket,
  TicketIntake,
  TicketPriority,
  TicketStatus,
} from '@wa-ticketing/shared';
import {
  mergeTicketIntake,
  defaultTicketIntake,
  isPortalChannelPhone,
  PORTAL_CHANNEL_PHONE,
} from '@wa-ticketing/shared';
import { auditService } from './auditService.js';
import { channelService } from './channelService.js';
import { resolveWhatsAppTarget, serializeEmbeddedChannel } from './channelProviderRules.js';
import { messagingService } from './messagingService.js';
import { sendSmtpMail } from './emailService.js';
import { emitMessageNew } from './messageEmitService.js';

export function mapTicketRow(
  row: Ticket & {
    updated_at?: string;
    portal_participant?: PortalParticipant | null;
    needs_escalation?: boolean;
  },
): Ticket {
  return {
    ...row,
    ...(row.channel !== undefined && { channel: serializeEmbeddedChannel(row.channel) }),
    updated_at: row.updated_at ?? row.created_at,
    needs_escalation: row.needs_escalation ?? false,
    intake: mergeTicketIntake(row.intake),
    portal_participant: row.portal_participant ?? undefined,
  };
}

export const ticketService = {
  async countForContactChannel(contactId: string, channelId: string): Promise<number> {
    const [row] = await sql<[{ c: string }]>`
      SELECT COUNT(*)::text AS c FROM tickets
      WHERE contact_id = ${contactId} AND channel_id = ${channelId}
    `;
    return Number.parseInt(row.c, 10);
  },

  /** First-ever ticket on this channel for this contact → onboarding eligible */
  buildInitialIntake(channelSettings: ChannelSettings, isFirstEverOnChannel: boolean): TicketIntake {
    if (!channelSettings.onboarding.enabled || !isFirstEverOnChannel) return { step: 'done', answers: {} };
    return { step: 'idle', answers: {} };
  },

  /**
   * Most recently updated open **direct** ticket for contact+channel.
   * Excludes WhatsApp group tickets (`wa_group_jid`) so 1:1 DMs never attach
   * to / reply into an open group thread.
   */
  async findOpenTicket(contactId: string, channelId: string): Promise<Ticket | null> {
    const [row] = await sql<[Ticket]>`
      SELECT * FROM tickets
      WHERE contact_id = ${contactId}
        AND channel_id  = ${channelId}
        AND status      = 'open'
        AND (wa_group_jid IS NULL OR btrim(wa_group_jid) = '')
      ORDER BY updated_at DESC, created_at DESC
      LIMIT 1
    `;
    return row ? mapTicketRow(row) : null;
  },

  async createTicket(
    contactId: string,
    channelId: string,
    intake: TicketIntake = defaultTicketIntake,
    opts?: {
      assignedTo?: string | null;
      actorId?: string | null;
      waGroupJid?: string | null;
      waGroupSubject?: string | null;
      waGroupParticipantUserIds?: string[];
    },
  ): Promise<Ticket> {
    const assignedTo = opts?.assignedTo ?? null;
    const waGroupJid = opts?.waGroupJid ?? null;
    const waGroupSubject = opts?.waGroupSubject ?? null;
    const participantIds = opts?.waGroupParticipantUserIds ?? [];
    const [ticket] = await sql<[Ticket]>`
      INSERT INTO tickets (
        contact_id, channel_id, intake, assigned_to,
        wa_group_jid, wa_group_subject, wa_group_participant_user_ids
      )
      VALUES (
        ${contactId},
        ${channelId},
        ${sql.json(intake as never)},
        ${assignedTo},
        ${waGroupJid},
        ${waGroupSubject},
        ${participantIds}
      )
      RETURNING *
    `;
    const actorId = opts?.actorId ?? null;
    await auditService.log(
      actorId,
      actorId ? 'user' : 'system',
      'ticket.created',
      'ticket',
      ticket.id,
      { contactId, channelId, assignedTo, waGroupJid },
    );
    return mapTicketRow(ticket);
  },

  async findOpenByGroupJid(channelId: string, groupJid: string): Promise<Ticket | null> {
    const [row] = await sql<[Ticket]>`
      SELECT * FROM tickets
      WHERE channel_id = ${channelId}
        AND wa_group_jid = ${groupJid}
        AND status = 'open'
      ORDER BY updated_at DESC, created_at DESC
      LIMIT 1
    `;
    return row ? mapTicketRow(row) : null;
  },

  async setGroupParticipantUserIds(ticketId: string, userIds: string[]): Promise<Ticket | null> {
    const [row] = await sql<[Ticket]>`
      UPDATE tickets
      SET wa_group_participant_user_ids = ${userIds}, updated_at = NOW()
      WHERE id = ${ticketId}
      RETURNING *
    `;
    return row ? mapTicketRow(row) : null;
  },

  async setSignupSessionId(ticketId: string, sessionId: string | null): Promise<void> {
    await sql`
      UPDATE tickets SET signup_session_id = ${sessionId}, updated_at = NOW()
      WHERE id = ${ticketId}
    `;
  },

  async closeTicket(ticketId: string, actorId: string, io?: SocketServer): Promise<Ticket> {
    const [pre] = await sql<[{ channel_id: string; phone_number: string; wa_group_jid: string | null }]>`
      SELECT t.channel_id, t.wa_group_jid, c.phone_number
      FROM tickets t
      INNER JOIN contacts c ON c.id = t.contact_id
      WHERE t.id = ${ticketId} AND t.status = 'open'
    `;
    if (!pre) throw Object.assign(new Error('Ticket not found or already closed'), { statusCode: 404 });

    const channel = await channelService.getById(pre.channel_id);
    const closing = channel?.settings?.ticketClosing;
    const closingText =
      closing?.enabled && closing.message?.trim() ? closing.message.trim() : null;

    const [ticket] = await sql<[Ticket]>`
      UPDATE tickets
      SET status = 'closed', closed_at = NOW(), updated_at = NOW()
      WHERE id = ${ticketId} AND status = 'open'
      RETURNING *
    `;
    if (!ticket) throw Object.assign(new Error('Ticket not found or already closed'), { statusCode: 404 });
    await auditService.log(actorId, 'user', 'ticket.closed', 'ticket', ticketId, {});

    if (io && closingText && channel) {
      try {
        if (isPortalChannelPhone(channel.phone_number)) {
          const message = await messagingService.appendMessage(
            ticketId,
            'outbound',
            closingText,
            null,
            undefined,
            'portal',
          );
          await auditService.log(actorId, 'user', 'ticket.closing_portal_sent', 'message', message.id, { ticketId });
          await emitMessageNew(io, ticketId, message);
        } else if (channel.kind === 'email' && pre.phone_number.startsWith('email:') && pre.phone_number.includes('@')) {
          const to = pre.phone_number.slice(6);
          const sent = await sendSmtpMail({
            to,
            subject: 'Ticket closed',
            text: closingText,
          });
          if (!sent.ok) throw new Error(sent.error ?? 'SMTP failed');
          const message = await messagingService.appendMessage(
            ticketId,
            'outbound',
            closingText,
            null,
            undefined,
            'email',
          );
          await auditService.log(actorId, 'user', 'ticket.closing_email_sent', 'message', message.id, { ticketId });
          await emitMessageNew(io, ticketId, message);
        } else {
          // Same target as staff sends; group tickets are provider-gated before anything is stored
          const waTarget = resolveWhatsAppTarget(channel, pre.wa_group_jid, pre.phone_number);
          const message = await messagingService.appendMessage(ticketId, 'outbound', closingText, null);
          const waMessageId = await channelService.sendMessage(pre.channel_id, waTarget, closingText);
          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', 'ticket.closing_whatsapp_sent', 'message', message.id, {
            ticketId,
          });
          await emitMessageNew(io, ticketId, message);
        }
      } catch (err) {
        await auditService.log(actorId, 'user', 'ticket.closing_delivery_failed', 'ticket', ticketId, {
          error: String(err),
        });
      }
    }

    return mapTicketRow(ticket);
  },

  async setPriority(ticketId: string, priority: TicketPriority, actorId: string): Promise<Ticket> {
    const [ticket] = await sql<[Ticket]>`
      UPDATE tickets SET priority = ${priority}, updated_at = NOW()
      WHERE id = ${ticketId}
      RETURNING *
    `;
    await auditService.log(actorId, 'user', 'ticket.priority_changed', 'ticket', ticketId, { priority });
    return mapTicketRow(ticket);
  },

  async setDueDate(ticketId: string, dueDate: string | null, actorId: string): Promise<Ticket> {
    const [ticket] = await sql<[Ticket]>`
      UPDATE tickets SET due_date = ${dueDate}, updated_at = NOW()
      WHERE id = ${ticketId}
      RETURNING *
    `;
    await auditService.log(actorId, 'user', 'ticket.due_date_set', 'ticket', ticketId, { dueDate });
    return mapTicketRow(ticket);
  },

  async assignTo(ticketId: string, userId: string | null, actorId: string): Promise<Ticket> {
    const [ticket] = await sql<[Ticket]>`
      UPDATE tickets SET assigned_to = ${userId}, updated_at = NOW()
      WHERE id = ${ticketId}
      RETURNING *
    `;
    await auditService.log(actorId, 'user', 'ticket.assigned', 'ticket', ticketId, { userId });
    return mapTicketRow(ticket);
  },

  async setNeedsEscalation(ticketId: string, value: boolean, actorId: string): Promise<Ticket> {
    if (!value) {
      const [existing] = await sql<Array<{ intake: unknown }>>`
        SELECT intake FROM tickets WHERE id = ${ticketId}
      `;
      const intake = mergeTicketIntake(existing?.intake);
      const next: TicketIntake = {
        ...intake,
        suppress_talkback: false,
        awaiting_human_confirm: false,
      };
      const aiRestored = Boolean(intake.suppress_talkback || intake.awaiting_human_confirm);
      const [ticket] = await sql<[Ticket]>`
        UPDATE tickets
        SET needs_escalation = false,
            intake = ${sql.json(next as never)},
            updated_at = NOW()
        WHERE id = ${ticketId}
        RETURNING *
      `;
      await auditService.log(actorId, 'user', 'ticket.escalation_flag', 'ticket', ticketId, {
        value,
        ...(aiRestored ? { ai_autoreply_restored: true } : {}),
      });
      return mapTicketRow(ticket);
    }

    const [ticket] = await sql<[Ticket]>`
      UPDATE tickets SET needs_escalation = ${value}, updated_at = NOW()
      WHERE id = ${ticketId}
      RETURNING *
    `;
    await auditService.log(actorId, 'user', 'ticket.escalation_flag', 'ticket', ticketId, { value });
    return mapTicketRow(ticket);
  },

  async getById(id: string): Promise<Ticket | null> {
    const portalPhone = PORTAL_CHANNEL_PHONE;
    const [row] = await sql<[Ticket]>`
      SELECT t.*,
        row_to_json(c.*) AS contact,
        row_to_json(ch.*) AS channel,
        json_build_object(
          'id', u.id,
          'email', u.email,
          'display_name', u.display_name
        ) AS assigned_user,
        CASE
          WHEN ch.phone_number = ${portalPhone}
            AND c.phone_number LIKE 'portal:%'
            AND portal_co.id IS NOT NULL
            AND portal_u.id IS NOT NULL
          THEN json_build_object(
            'company_name', portal_co.name,
            'user_email', portal_u.email,
            'user_display_name', portal_u.display_name
          )
          ELSE NULL
        END AS portal_participant,
        (
          SELECT MAX(m.created_at)
          FROM messages m
          WHERE m.ticket_id = t.id
        ) AS last_message_at
      FROM tickets t
      LEFT JOIN contacts c  ON c.id  = t.contact_id
      LEFT JOIN channels ch ON ch.id = t.channel_id
      LEFT JOIN users u     ON u.id  = t.assigned_to
      LEFT JOIN companies portal_co
        ON portal_co.id = c.company_id AND ch.phone_number = ${portalPhone}
      LEFT JOIN users portal_u
        ON ch.phone_number = ${portalPhone}
        AND c.phone_number LIKE 'portal:%'
        AND portal_u.id::text = substring(c.phone_number from 8 for 36)
      WHERE t.id = ${id}
    `;
    return row ? mapTicketRow(row) : null;
  },

  async list(filters: {
    status?: TicketStatus;
    priority?: TicketPriority;
    channelId?: string;
    assignedTo?: string;
    from?: string;
    to?: string;
    limit?: number;
    offset?: number;
    companyId?: string;
    /** Portal users: restrict to their own portal contact tickets */
    companyUserContext?: { userId: string; companyId: string };
  }) {
    const {
      status,
      priority,
      channelId,
      assignedTo,
      from,
      to,
      companyId,
      companyUserContext,
      limit = 50,
      offset = 0,
    } = filters;

    const portalContactKey = companyUserContext ? `portal:${companyUserContext.userId}` : null;

    const portalPhone = PORTAL_CHANNEL_PHONE;
    const rows = await sql<Ticket[]>`
      SELECT t.*,
        row_to_json(c.*) AS contact,
        row_to_json(ch.*) AS channel,
        json_build_object(
          'id', u.id,
          'email', u.email,
          'display_name', u.display_name
        ) AS assigned_user,
        CASE
          WHEN ch.phone_number = ${portalPhone}
            AND c.phone_number LIKE 'portal:%'
            AND portal_co.id IS NOT NULL
            AND portal_u.id IS NOT NULL
          THEN json_build_object(
            'company_name', portal_co.name,
            'user_email', portal_u.email,
            'user_display_name', portal_u.display_name
          )
          ELSE NULL
        END AS portal_participant,
        (
          SELECT MAX(m.created_at)
          FROM messages m
          WHERE m.ticket_id = t.id
        ) AS last_message_at
      FROM tickets t
      LEFT JOIN contacts c  ON c.id  = t.contact_id
      LEFT JOIN channels ch ON ch.id = t.channel_id
      LEFT JOIN users u     ON u.id  = t.assigned_to
      LEFT JOIN companies portal_co
        ON portal_co.id = c.company_id AND ch.phone_number = ${portalPhone}
      LEFT JOIN users portal_u
        ON ch.phone_number = ${portalPhone}
        AND c.phone_number LIKE 'portal:%'
        AND portal_u.id::text = substring(c.phone_number from 8 for 36)
      WHERE TRUE
        ${status     ? sql`AND t.status      = ${status}`     : sql``}
        ${priority   ? sql`AND t.priority    = ${priority}`   : sql``}
        ${channelId  ? sql`AND t.channel_id  = ${channelId}`  : sql``}
        ${assignedTo === 'unassigned'
          ? sql`AND t.assigned_to IS NULL`
          : assignedTo
            ? sql`AND t.assigned_to = ${assignedTo}`
            : sql``}
        ${from       ? sql`AND t.created_at >= ${from}`       : sql``}
        ${to         ? sql`AND t.created_at <= ${to}`         : sql``}
        ${companyUserContext
          ? sql`AND c.company_id = ${companyUserContext.companyId} AND c.phone_number = ${portalContactKey}`
          : companyId
            ? sql`AND c.company_id = ${companyId}`
            : sql``}
      ORDER BY t.updated_at DESC
      LIMIT ${limit} OFFSET ${offset}
    `;
    return rows.map(mapTicketRow);
  },
};
