/**
 * Conversation lifecycle: create from the pre-chat form, queue, assign,
 * transfer, close. Every mutation returns hydrated DTOs so the socket layer
 * can broadcast without a second round trip.
 */
import { many, one, query, tx } from '../../db/pool.js';
import { formatDevice, formatGeo, type Enrichment } from '../../enrichment/index.js';
import { notFound, conflict, badRequest } from '../../utils/errors.js';
import type {
  ConversationDTO,
  CustomerDTO,
  HistoryEntryDTO,
  SessionInfoDTO,
} from '../types.js';

export interface StartChatInput {
  name: string;
  email?: string | null;
  phone?: string | null;
  issue: string;
  priority?: number;
  meta?: Record<string, unknown>;
  enrichment: Enrichment;
}

/* ------------------------------------------------------------------ */
/* Row → DTO mapping                                                    */
/* ------------------------------------------------------------------ */

function toSessionDTO(row: any): SessionInfoDTO | null {
  if (!row || !row.session_id) return null;
  const device = row.device ?? {};
  const geo = row.geo ?? {};
  return {
    ip: row.ip ?? null,
    userAgent: row.user_agent ?? null,
    device,
    geo,
    locale: row.locale ?? null,
    timezone: row.timezone ?? null,
    screen: row.screen ?? null,
    pageUrl: row.page_url ?? null,
    referrer: row.referrer ?? null,
    deviceLabel: formatDevice(device),
    geoLabel: formatGeo(geo),
  };
}

export function toConversationDTO(row: any): ConversationDTO {
  const now = Date.now();
  const startedWaiting = row.accepted_at ? new Date(row.accepted_at) : new Date();
  const queuedAt = new Date(row.queued_at);
  return {
    id: row.id,
    status: row.status,
    priority: row.priority,
    subject: row.subject ?? null,
    customer: {
      id: row.customer_id,
      name: row.customer_name,
      email: row.customer_email ?? null,
      phone: row.customer_phone ?? null,
      createdAt: new Date(row.customer_created_at).toISOString(),
    },
    agent: row.agent_id
      ? { id: row.agent_id, name: row.agent_name, avatarUrl: row.agent_avatar ?? null }
      : null,
    session: toSessionDTO(row),
    lastMessage: row.last_message_body
      ? {
          body: row.last_message_body,
          createdAt: new Date(row.last_message_at).toISOString(),
          senderType: row.last_message_sender,
        }
      : null,
    unreadForAgent: Number(row.unread_for_agent ?? 0),
    customerLastReadSeq: Number(row.customer_last_read_seq ?? 0),
    agentLastReadSeq: Number(row.agent_last_read_seq ?? 0),
    queuedAt: queuedAt.toISOString(),
    acceptedAt: row.accepted_at ? new Date(row.accepted_at).toISOString() : null,
    closedAt: row.closed_at ? new Date(row.closed_at).toISOString() : null,
    lastMessageAt: row.last_message_at ? new Date(row.last_message_at).toISOString() : null,
    waitingSeconds: Math.max(
      0,
      Math.round(((row.accepted_at ? startedWaiting.getTime() : now) - queuedAt.getTime()) / 1000),
    ),
    meta: row.meta ?? {},
  };
}

/** Single query that hydrates a conversation with customer, agent, session, unread count. */
const CONVERSATION_SELECT = `
  SELECT c.id, c.status, c.priority, c.subject, c.queued_at, c.accepted_at, c.closed_at,
         c.last_message_at, c.meta, c.customer_last_read_seq, c.agent_last_read_seq,
         cu.id AS customer_id, cu.name AS customer_name, cu.email AS customer_email,
         cu.phone AS customer_phone, cu.created_at AS customer_created_at,
         a.id AS agent_id, a.name AS agent_name, a.avatar_url AS agent_avatar,
         s.id AS session_id, s.ip, s.user_agent, s.device, s.geo, s.locale,
         s.timezone, s.screen, s.page_url, s.referrer,
         lm.body AS last_message_body, lm.sender_type AS last_message_sender,
         (SELECT COUNT(*) FROM messages m
           WHERE m.conversation_id = c.id
             AND m.seq > c.agent_last_read_seq
             AND m.sender_type = 'customer') AS unread_for_agent
    FROM conversations c
    JOIN customers cu ON cu.id = c.customer_id
    LEFT JOIN agents a ON a.id = c.agent_id
    LEFT JOIN customer_sessions s ON s.conversation_id = c.id
    LEFT JOIN LATERAL (
      SELECT body, sender_type FROM messages
       WHERE conversation_id = c.id ORDER BY seq DESC LIMIT 1
    ) lm ON TRUE
`;

/* ------------------------------------------------------------------ */
/* Reads                                                               */
/* ------------------------------------------------------------------ */

export async function getConversation(id: string): Promise<ConversationDTO> {
  const row = await one(`${CONVERSATION_SELECT} WHERE c.id = $1`, [id]);
  if (!row) throw notFound('Conversation not found');
  return toConversationDTO(row);
}

export async function listQueued(): Promise<ConversationDTO[]> {
  const rows = await many(
    `${CONVERSATION_SELECT} WHERE c.status = 'queued' ORDER BY c.priority DESC, c.queued_at ASC LIMIT 100`,
  );
  return rows.map(toConversationDTO);
}

export async function listForAgent(agentId: string): Promise<ConversationDTO[]> {
  const rows = await many(
    `${CONVERSATION_SELECT}
      WHERE c.agent_id = $1 AND c.status = 'active'
      ORDER BY c.priority DESC, c.last_message_at DESC NULLS LAST`,
    [agentId],
  );
  return rows.map(toConversationDTO);
}

/** Previous chats for the same person — shown in the agent's profile panel. */
export async function listCustomerHistory(
  customerId: string,
  excludeConversationId?: string,
): Promise<HistoryEntryDTO[]> {
  const rows = await many(
    `SELECT c.id, c.subject, c.status, c.created_at, c.closed_at,
            a.name AS agent_name,
            (SELECT COUNT(*) FROM messages m WHERE m.conversation_id = c.id) AS message_count,
            r.rating AS csat_rating
       FROM conversations c
       LEFT JOIN agents a ON a.id = c.agent_id
       LEFT JOIN csat_ratings r ON r.conversation_id = c.id
      WHERE c.customer_id = $1 AND ($2::uuid IS NULL OR c.id <> $2)
      ORDER BY c.created_at DESC
      LIMIT 20`,
    [customerId, excludeConversationId ?? null],
  );
  return rows.map((r: any) => ({
    id: r.id,
    subject: r.subject ?? null,
    status: r.status,
    createdAt: new Date(r.created_at).toISOString(),
    closedAt: r.closed_at ? new Date(r.closed_at).toISOString() : null,
    agentName: r.agent_name ?? null,
    messageCount: Number(r.message_count),
    csatRating: r.csat_rating ?? null,
  }));
}

/* ------------------------------------------------------------------ */
/* Writes                                                              */
/* ------------------------------------------------------------------ */

/**
 * Pre-chat form submit. Creates (or re-uses) the customer, opens a queued
 * conversation, and stores the enrichment snapshot — all in one transaction so
 * the agent can never see a half-populated profile.
 */
export async function startChat(input: StartChatInput): Promise<ConversationDTO> {
  const name = input.name.trim();
  const email = input.email?.trim().toLowerCase() || null;
  const phone = input.phone?.trim() || null;
  const issue = input.issue.trim();
  if (!name) throw badRequest('Name is required');
  if (!issue) throw badRequest('Issue description is required');

  const conversationId = await tx(async (client) => {
    // Re-identify a returning customer by email, then phone.
    let customer = email
      ? (await client.query(`SELECT * FROM customers WHERE email = $1`, [email])).rows[0]
      : null;
    if (!customer && phone) {
      customer = (
        await client.query(`SELECT * FROM customers WHERE phone = $1 ORDER BY created_at LIMIT 1`, [phone])
      ).rows[0];
    }

    if (customer) {
      // Keep the freshest contact details without wiping known ones.
      customer = (
        await client.query(
          `UPDATE customers
              SET name = $2, email = COALESCE($3, email), phone = COALESCE($4, phone)
            WHERE id = $1 RETURNING *`,
          [customer.id, name, email, phone],
        )
      ).rows[0];
    } else {
      customer = (
        await client.query(
          `INSERT INTO customers (name, email, phone) VALUES ($1, $2, $3) RETURNING *`,
          [name, email, phone],
        )
      ).rows[0];
    }

    const conv = (
      await client.query(
        `INSERT INTO conversations (customer_id, subject, priority, meta, status)
         VALUES ($1, $2, $3, $4, 'queued') RETURNING id`,
        [customer.id, issue, input.priority ?? 0, JSON.stringify(input.meta ?? {})],
      )
    ).rows[0];

    const e = input.enrichment;
    await client.query(
      `INSERT INTO customer_sessions
         (conversation_id, customer_id, ip, user_agent, device, geo, locale, timezone, screen, page_url, referrer)
       VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11)
       ON CONFLICT (conversation_id) DO NOTHING`,
      [
        conv.id,
        customer.id,
        e.ip || null,
        e.userAgent || null,
        JSON.stringify(e.device ?? {}),
        JSON.stringify(e.geo ?? {}),
        e.locale ?? null,
        e.timezone ?? null,
        e.screen ? JSON.stringify(e.screen) : null,
        e.pageUrl ?? null,
        e.referrer ?? null,
      ],
    );

    // The issue text is also the customer's opening message, so the agent
    // reads it in the transcript as well as the profile panel.
    await client.query(
      `INSERT INTO messages (conversation_id, sender_type, sender_id, type, body)
       VALUES ($1, 'customer', $2, 'text', $3)`,
      [conv.id, customer.id, issue],
    );
    await client.query(
      `UPDATE conversations SET last_message_at = now() WHERE id = $1`,
      [conv.id],
    );

    return conv.id as string;
  });

  return getConversation(conversationId);
}

/**
 * Claim a queued conversation. The conditional UPDATE is the concurrency
 * guard: two agents clicking "Accept" at once, one wins, the other gets 409.
 */
export async function assignAgent(conversationId: string, agentId: string): Promise<ConversationDTO> {
  const updated = await one(
    `UPDATE conversations
        SET agent_id = $2, status = 'active', accepted_at = COALESCE(accepted_at, now())
      WHERE id = $1 AND status = 'queued'
      RETURNING id`,
    [conversationId, agentId],
  );
  if (!updated) {
    const current = await one(`SELECT status, agent_id FROM conversations WHERE id = $1`, [conversationId]);
    if (!current) throw notFound('Conversation not found');
    throw conflict('This chat was already picked up by another agent');
  }
  return getConversation(conversationId);
}

export async function transferConversation(
  conversationId: string,
  fromAgentId: string,
  toAgentId: string,
  note?: string,
): Promise<ConversationDTO> {
  if (fromAgentId === toAgentId) throw badRequest('Cannot transfer a chat to yourself');

  await tx(async (client) => {
    const conv = (
      await client.query(`SELECT status FROM conversations WHERE id = $1 FOR UPDATE`, [conversationId])
    ).rows[0];
    if (!conv) throw notFound('Conversation not found');
    if (conv.status === 'closed') throw conflict('Cannot transfer a closed chat');

    const target = (await client.query(`SELECT id FROM agents WHERE id = $1`, [toAgentId])).rows[0];
    if (!target) throw notFound('Target agent not found');

    await client.query(
      `UPDATE conversations SET agent_id = $2, status = 'active' WHERE id = $1`,
      [conversationId, toAgentId],
    );
    await client.query(
      `INSERT INTO conversation_transfers (conversation_id, from_agent_id, to_agent_id, note)
       VALUES ($1, $2, $3, $4)`,
      [conversationId, fromAgentId, toAgentId, note ?? null],
    );
  });

  return getConversation(conversationId);
}

export async function closeConversation(conversationId: string, reason?: string): Promise<ConversationDTO> {
  const updated = await one(
    `UPDATE conversations
        SET status = 'closed', closed_at = COALESCE(closed_at, now()), close_reason = $2
      WHERE id = $1 RETURNING id`,
    [conversationId, reason ?? null],
  );
  if (!updated) throw notFound('Conversation not found');
  return getConversation(conversationId);
}

/** Requeue a chat whose agent disconnected for good (used on agent logout). */
export async function requeueConversation(conversationId: string): Promise<ConversationDTO> {
  await query(
    `UPDATE conversations
        SET status = 'queued', agent_id = NULL, accepted_at = NULL, queued_at = now()
      WHERE id = $1 AND status = 'active'`,
    [conversationId],
  );
  return getConversation(conversationId);
}

export async function setPriority(conversationId: string, priority: number): Promise<ConversationDTO> {
  if (![0, 1, 2].includes(priority)) throw badRequest('Priority must be 0, 1 or 2');
  await query(`UPDATE conversations SET priority = $2 WHERE id = $1`, [conversationId, priority]);
  return getConversation(conversationId);
}

export async function getCustomer(customerId: string): Promise<CustomerDTO> {
  const row = await one(`SELECT * FROM customers WHERE id = $1`, [customerId]);
  if (!row) throw notFound('Customer not found');
  return {
    id: row.id,
    name: row.name,
    email: row.email ?? null,
    phone: row.phone ?? null,
    createdAt: new Date(row.created_at).toISOString(),
  };
}

/** Is this agent allowed to act on this conversation? */
export async function assertAgentOwns(conversationId: string, agentId: string): Promise<void> {
  const row = await one(`SELECT agent_id FROM conversations WHERE id = $1`, [conversationId]);
  if (!row) throw notFound('Conversation not found');
  if (row.agent_id !== agentId) throw conflict('This chat is assigned to a different agent');
}
