import type { PageParams, Paginated } from '@/lib/api/types';

/**
 * Tickets domain — the **only sanctioned post-booking channel** (b15). There is no live chat and no direct
 * nurse↔customer messaging by design: every conversation is a ticket that admin can read in full
 * (anti-disintermediation + patient safety — see `product/business/12-messaging-and-emergencies.md`). A
 * booking-coordination ticket is auto-created on confirmation; users also open support/refund tickets.
 *
 * Shapes derive from the b15 contract (`dev/contracts/domains/messaging-notifications-admin.md` +
 * `dev/contracts/openapi/swagger.v1.json` → `TicketSummaryDto`/`TicketThreadDto`/…), mapped to a client
 * model that carries the display state the wire omits.
 *
 * **Load-bearing rules (contract §"Critical rules" + phase §5):**
 *  - **`is_internal` NEVER reaches the user app.** The server strips internal admin messages from the user
 *    view (`GET /tickets/{id}`). We do **not** model an `isInternal` field here, never render internal
 *    styling, and never expose an internal-note affordance — and the real client mapper drops any message
 *    that arrives flagged internal (a defensive guard; such a leak is a backend defect to file, not render).
 *  - **`referenceCode` is stable + unique** (`"TKT-9F3K2A7Q"`) — quoted to support, shown prominently.
 *  - **Ticket↔booking/refund links are optional** — `bookingId`/`refundId` are both nullable.
 *  - **No phone numbers, ever.** The only sanctioned out-of-band surface is the post-confirmation
 *    emergency `tel:` (from the f8 care-instructions read), never a contact directory.
 *
 * Enums cross the wire as stable string codes — mirrored here as string-literal unions.
 */

/** `ticket.status` (contract enum). */
export type TicketStatus = 'open' | 'closed';

/** `ticket.category` (contract enum). A booking-coordination ticket is auto-created on confirmation. */
export type TicketCategory = 'coordination' | 'support' | 'refund' | 'emergency';

/**
 * The display role of a message author (from `ticket_participants.role_on_ticket`). `admin` renders as
 * "support" in the user app — it is a **display label, never an auth source**. `system` is the fallback
 * when a sender isn't in the participant list.
 */
export type TicketAuthorRole = 'customer' | 'nurse' | 'admin' | 'system';

/** Per-message optimistic send state — `sent` for any server-confirmed message. */
export type MessageSendStatus = 'sent' | 'sending' | 'failed';

/**
 * A ticket row for the "My Tickets" inbox (`TicketSummaryDto`). `lastMessageAt`/`unreadCount` are **not**
 * on the wire summary (REQ-028) — they are optional and only the mock supplies them today; the inbox
 * renders the unread indicator / last-activity time only when present, else falls back to `createdAt`.
 */
export interface TicketSummary {
  id: number;
  referenceCode: string;
  subject: string | null;
  status: TicketStatus;
  category: TicketCategory;
  bookingId: number | null;
  refundId: number | null;
  createdAt: string;
  /** REQ-028 gap — the wire summary has no last-activity timestamp; mock-only until delivered. */
  lastMessageAt?: string | null;
  /** REQ-028 gap — the wire summary has no unread count; mock-only until delivered. */
  unreadCount?: number;
}

/** A participant on a ticket (`TicketParticipantDto`) — used to derive a message's author role. */
export interface TicketParticipant {
  userId: number;
  roleOnTicket: TicketAuthorRole;
}

/**
 * A single message in a thread (client model). The wire `TicketMessageDto` carries only `senderId` (no
 * author name, REQ-028) — we derive `authorRole` from the participant list and `isMine` from the viewer,
 * and never show a raw name (privacy: the platform never turns a thread into a contact directory).
 */
export interface TicketMessage {
  /** Server message id; `null` while an optimistic message is still pending. */
  id: number | null;
  /** Client-generated id for an optimistic message; the reconcile key so we never double-render (§3.5). */
  clientMessageId?: string;
  ticketId: number;
  body: string;
  authorRole: TicketAuthorRole;
  /** UTC ISO-8601 — Shamsi display is the client's job. */
  createdAt: string;
  isMine: boolean;
  sendStatus: MessageSendStatus;
}

/**
 * The full thread (`TicketThreadDto`, user view). The contract returns the whole `messages[]` in one call
 * (no server pagination), so this is the single cached detail; `useTicketThread` is a `select` over it.
 */
export interface TicketDetail {
  id: number;
  referenceCode: string;
  subject: string | null;
  status: TicketStatus;
  category: TicketCategory;
  bookingId: number | null;
  refundId: number | null;
  openedById: number;
  closedAt: string | null;
  participants: TicketParticipant[];
  messages: TicketMessage[];
}

/** List filters for `GET /tickets` (own, paginated). `status` optionally narrows the inbox. */
export interface TicketListParams extends PageParams {
  status?: TicketStatus;
}

/** `OpenTicketCommand`. `bookingId`/`refundId` optional; a booking link requires the caller be a party. */
export interface OpenTicketRequest {
  category: TicketCategory;
  subject?: string | null;
  body: string;
  bookingId?: number | null;
  refundId?: number | null;
}

/** `OpenTicketResult` — the new ticket's stable `referenceCode` (shown on the confirmation). */
export interface OpenTicketResult {
  ticketId: number;
  referenceCode: string;
  status: TicketStatus;
  category: TicketCategory;
}

/**
 * `PostMessageCommand` body. `clientMessageId` is client-generated for optimistic reconciliation; the
 * server has no field for it today (REQ-028 — an idempotency key), so the real client does not send it — it
 * lives only in the cache to reconcile the pending bubble by identity.
 */
export interface PostMessageRequest {
  body: string;
  clientMessageId: string;
}

/** `PostMessageResult`. */
export interface PostMessageResult {
  messageId: number;
  ticketId: number;
  sentAt: string;
}

/* ── Admin ticket lens (b15 `GET /admin/tickets…`) ───────────────────────────────────────────────────
 * The **admin** surface is a DELIBERATELY SEPARATE model from the user types above. The admin view
 * INCLUDES internal notes (`GET /admin/tickets/{id}`), so its message/detail shapes carry `isInternal` —
 * a flag the user types (`TicketMessage`/`TicketDetail`) must NEVER gain (an internal note can't bleed
 * into the user app; contract "Critical rules" + phase §5). Keeping the two surfaces distinct is the
 * enforcement: there is no `isInternal` on the user side to accidentally read/render.
 */

/** Admin thread message — INCLUDES the internal-note flag (user types never do). */
export interface AdminTicketMessage {
  id: number | null;
  clientMessageId?: string;
  ticketId: number;
  body: string;
  authorRole: TicketAuthorRole;
  createdAt: string;
  isMine: boolean;
  isInternal: boolean;
  sendStatus: MessageSendStatus;
}
export interface AdminTicketDetail {
  id: number;
  referenceCode: string;
  subject: string | null;
  status: TicketStatus;
  category: TicketCategory;
  bookingId: number | null;
  refundId: number | null;
  openedById: number;
  closedAt: string | null;
  participants: TicketParticipant[];
  messages: AdminTicketMessage[];
}
export interface AdminTicketSummary {
  id: number;
  referenceCode: string;
  subject: string | null;
  status: TicketStatus;
  category: TicketCategory;
  bookingId: number | null;
  refundId: number | null;
  createdAt: string;
}
export interface AdminTicketFilters {
  status?: TicketStatus;
  category?: TicketCategory;
  referenceCode?: string;
  bookingId?: number;
  refundId?: number;
}
export interface PostAdminMessageRequest {
  body: string;
  isInternal: boolean;
  clientMessageId: string;
}

/**
 * The tickets API seam — the real HTTP client and the in-memory mock both implement this; selection is by
 * config (`USE_TICKETS_MOCK`), never scattered `if (mock)` checks. `getTicket` takes the viewer's user id
 * so the real mapper can compute `isMine`; the mock is a self-contained world (its own "me").
 */
export interface TicketsApi {
  listMyTickets(params: TicketListParams): Promise<Paginated<TicketSummary>>;
  /** The full thread (user view — internal messages stripped). `viewerUserId` drives `isMine`. */
  getTicket(ticketId: number, viewerUserId?: number): Promise<TicketDetail>;
  /**
   * Open a ticket. `viewerUserId` is the opener's id — the real server infers the sender from auth, but the
   * mock needs it to attribute the opening message to the right actor (customer vs nurse) and add them as a
   * participant, so an optimistic/rendered opener bubble is correctly "mine".
   */
  openTicket(body: OpenTicketRequest, viewerUserId?: number): Promise<OpenTicketResult>;
  postMessage(ticketId: number, body: PostMessageRequest): Promise<PostMessageResult>;

  /* Admin lens (b15). Distinct methods so the internal-carrying admin view can never be reached through a
   * user-view call. `listAdminTickets` is the global queue (all tickets, filterable); `getAdminTicket`
   * returns the thread WITH internal notes; `postAdminMessage` may set `isInternal` (staff only). */
  listAdminTickets(filters: AdminTicketFilters, params: PageParams): Promise<Paginated<AdminTicketSummary>>;
  getAdminTicket(ticketId: number, viewerUserId?: number): Promise<AdminTicketDetail>;
  postAdminMessage(ticketId: number, body: PostAdminMessageRequest): Promise<PostMessageResult>;
}
