import { clientFetch } from '@/lib/api/client';
import { unwrap, type ApiEnvelope, type PageParams, type Paginated } from '@/lib/api/types';
import { TICKETS_PAGE_SIZE } from '../constants';
import type {
  AdminTicketDetail,
  AdminTicketFilters,
  AdminTicketMessage,
  AdminTicketSummary,
  OpenTicketRequest,
  OpenTicketResult,
  PostAdminMessageRequest,
  PostMessageRequest,
  PostMessageResult,
  TicketAuthorRole,
  TicketDetail,
  TicketListParams,
  TicketMessage,
  TicketSummary,
  TicketsApi,
} from '../types';

const API = '/api/v1';

/** Wire `TicketSummaryDto` (camelCase). REQ-028 (delivered) added `lastMessageAt`/`unreadCount`. */
interface TicketSummaryWire {
  id: number;
  referenceCode: string;
  subject: string | null;
  status: string;
  category: string;
  bookingId: number | null;
  refundId: number | null;
  createdAt: string;
  lastMessageAt: string | null;
  unreadCount: number;
}

/** Wire `TicketMessageDto`. `isInternal` is present on the DTO but is `false` in the user view (server-stripped). */
interface TicketMessageWire {
  id: number;
  senderId: number;
  body: string;
  isInternal: boolean;
  sentAt: string;
}

/** Wire `TicketThreadDto` (user view). */
interface TicketThreadWire {
  id: number;
  referenceCode: string;
  subject: string | null;
  status: string;
  category: string;
  bookingId: number | null;
  refundId: number | null;
  openedById: number;
  closedAt: string | null;
  participants: Array<{ userId: number; roleOnTicket: string | null }>;
  messages: TicketMessageWire[];
}

function mapSummary(w: TicketSummaryWire): TicketSummary {
  return {
    id: w.id,
    referenceCode: w.referenceCode,
    subject: w.subject,
    status: w.status as TicketSummary['status'],
    category: w.category as TicketSummary['category'],
    bookingId: w.bookingId,
    refundId: w.refundId,
    createdAt: w.createdAt,
    // REQ-028 (delivered): the inbox unread badge + last-activity sort now come off the wire.
    lastMessageAt: w.lastMessageAt,
    unreadCount: w.unreadCount,
  };
}

function mapThread(w: TicketThreadWire, viewerUserId?: number): TicketDetail {
  const roleBySender = new Map<number, TicketAuthorRole>(
    w.participants.map((p) => [p.userId, (p.roleOnTicket ?? 'system') as TicketAuthorRole]),
  );
  const messages: TicketMessage[] = w.messages
    // Defensive: the user view is server-stripped of internal notes, but never render one if it leaks —
    // that is a backend defect to file, not to surface (phase §5, contract "Critical rules").
    .filter((m) => !m.isInternal)
    .map((m) => ({
      id: m.id,
      ticketId: w.id,
      body: m.body,
      authorRole: roleBySender.get(m.senderId) ?? 'system',
      createdAt: m.sentAt,
      isMine: viewerUserId != null && m.senderId === viewerUserId,
      sendStatus: 'sent' as const,
    }));
  return {
    id: w.id,
    referenceCode: w.referenceCode,
    subject: w.subject,
    status: w.status as TicketDetail['status'],
    category: w.category as TicketDetail['category'],
    bookingId: w.bookingId,
    refundId: w.refundId,
    openedById: w.openedById,
    closedAt: w.closedAt,
    participants: w.participants.map((p) => ({
      userId: p.userId,
      roleOnTicket: (p.roleOnTicket ?? 'system') as TicketAuthorRole,
    })),
    messages,
  };
}

/** Admin summary shares the wire shape of the user summary (no `unread`/`lastMessageAt`). */
function mapAdminSummary(w: TicketSummaryWire): AdminTicketSummary {
  return {
    id: w.id,
    referenceCode: w.referenceCode,
    subject: w.subject,
    status: w.status as AdminTicketSummary['status'],
    category: w.category as AdminTicketSummary['category'],
    bookingId: w.bookingId,
    refundId: w.refundId,
    createdAt: w.createdAt,
  };
}

/**
 * Admin thread mapper. Unlike `mapThread`, it **keeps internal messages** and carries `isInternal` through —
 * this IS the admin view, whose whole purpose is that staff see internal notes (contract §"Critical rules").
 */
function mapAdminThread(w: TicketThreadWire, viewerUserId?: number): AdminTicketDetail {
  const roleBySender = new Map<number, TicketAuthorRole>(
    w.participants.map((p) => [p.userId, (p.roleOnTicket ?? 'system') as TicketAuthorRole]),
  );
  const messages: AdminTicketMessage[] = w.messages.map((m) => ({
    id: m.id,
    ticketId: w.id,
    body: m.body,
    authorRole: roleBySender.get(m.senderId) ?? 'system',
    createdAt: m.sentAt,
    isMine: viewerUserId != null && m.senderId === viewerUserId,
    isInternal: m.isInternal,
    sendStatus: 'sent' as const,
  }));
  return {
    id: w.id,
    referenceCode: w.referenceCode,
    subject: w.subject,
    status: w.status as AdminTicketDetail['status'],
    category: w.category as AdminTicketDetail['category'],
    bookingId: w.bookingId,
    refundId: w.refundId,
    openedById: w.openedById,
    closedAt: w.closedAt,
    participants: w.participants.map((p) => ({
      userId: p.userId,
      roleOnTicket: (p.roleOnTicket ?? 'system') as TicketAuthorRole,
    })),
    messages,
  };
}

/**
 * Real HTTP implementation of the `TicketsApi` seam (b15 contract). All four methods map published routes:
 *  - `listMyTickets` → `GET /tickets` (own, paginated; `Status`/`ReferenceCode`/`Page`/`PageSize`).
 *  - `getTicket`     → `GET /tickets/{id}` (user view — internal messages already stripped server-side).
 *  - `openTicket`    → `POST /tickets`.
 *  - `postMessage`   → `POST /tickets/{id}/messages` (a non-staff caller never sets `isInternal`).
 *
 * PRIMARY once `USE_TICKETS_MOCK = false` (refinement-phase-4; REQ-028 delivered): the summary now carries
 * `unreadCount`/`lastMessageAt` (inbox badge + last-activity sort) and the message post sends the optimistic
 * `clientMessageId` (server dedupes + echoes it back). The user list still filters only by `Status`; the
 * "jump to the existing coordination ticket" by-booking lookup is a minor follow-up (REQ-028 #3 —
 * `GET /tickets?BookingId=` is served, but no client method targets it yet).
 */
export const ticketsClientApi: TicketsApi = {
  listMyTickets: async (params: TicketListParams): Promise<Paginated<TicketSummary>> => {
    const query = new URLSearchParams();
    if (params.status) query.set('Status', params.status);
    query.set('Page', String(params.page ?? 1));
    query.set('PageSize', String(params.pageSize ?? TICKETS_PAGE_SIZE));
    const wire = unwrap(
      await clientFetch<ApiEnvelope<Paginated<TicketSummaryWire>>>(`${API}/tickets?${query.toString()}`),
    );
    return { ...wire, items: wire.items.map(mapSummary) };
  },

  getTicket: async (ticketId: number, viewerUserId?: number): Promise<TicketDetail> => {
    const wire = unwrap(await clientFetch<ApiEnvelope<TicketThreadWire>>(`${API}/tickets/${ticketId}`));
    return mapThread(wire, viewerUserId);
  },

  // The server infers the opener from auth — `viewerUserId` is only for the mock's attribution.
  openTicket: async (body: OpenTicketRequest, _viewerUserId?: number): Promise<OpenTicketResult> =>
    unwrap(
      await clientFetch<ApiEnvelope<OpenTicketResult>>(`${API}/tickets`, {
        method: 'POST',
        body: JSON.stringify({
          category: body.category,
          subject: body.subject ?? null,
          body: body.body,
          bookingId: body.bookingId ?? null,
          refundId: body.refundId ?? null,
        }),
      }),
    ),

  postMessage: async (ticketId: number, body: PostMessageRequest): Promise<PostMessageResult> =>
    unwrap(
      await clientFetch<ApiEnvelope<PostMessageResult>>(`${API}/tickets/${ticketId}/messages`, {
        method: 'POST',
        // REQ-028 (delivered): send the optimistic `clientMessageId` so the server dedupes a retried send
        // and echoes it back on `PostMessageResult` for reconciliation.
        body: JSON.stringify({ body: body.body, clientMessageId: body.clientMessageId }),
      }),
    ),

  // ── Admin lens (b15). Global queue + admin thread (internal INCLUDED) + staff message post. ──
  listAdminTickets: async (
    filters: AdminTicketFilters,
    params: PageParams,
  ): Promise<Paginated<AdminTicketSummary>> => {
    const query = new URLSearchParams();
    if (filters.status) query.set('Status', filters.status);
    if (filters.category) query.set('Category', filters.category);
    if (filters.referenceCode) query.set('ReferenceCode', filters.referenceCode);
    if (filters.bookingId != null) query.set('BookingId', String(filters.bookingId));
    if (filters.refundId != null) query.set('RefundId', String(filters.refundId));
    query.set('Page', String(params.page ?? 1));
    query.set('PageSize', String(params.pageSize ?? TICKETS_PAGE_SIZE));
    const wire = unwrap(
      await clientFetch<ApiEnvelope<Paginated<TicketSummaryWire>>>(
        `${API}/admin/tickets?${query.toString()}`,
      ),
    );
    return { ...wire, items: wire.items.map(mapAdminSummary) };
  },

  getAdminTicket: async (ticketId: number, viewerUserId?: number): Promise<AdminTicketDetail> => {
    // The admin view keeps internal notes — do NOT filter them here; that is the point of this endpoint.
    const wire = unwrap(await clientFetch<ApiEnvelope<TicketThreadWire>>(`${API}/admin/tickets/${ticketId}`));
    return mapAdminThread(wire, viewerUserId);
  },

  // Staff post — may set `isInternal` (the one caller allowed to). REQ-028: send `clientMessageId` too.
  postAdminMessage: async (ticketId: number, body: PostAdminMessageRequest): Promise<PostMessageResult> =>
    unwrap(
      await clientFetch<ApiEnvelope<PostMessageResult>>(`${API}/tickets/${ticketId}/messages`, {
        method: 'POST',
        body: JSON.stringify({
          body: body.body,
          isInternal: body.isInternal,
          clientMessageId: body.clientMessageId,
        }),
      }),
    ),

};
