/**
 * Tauri adapter for {@link TicketGateway}.
 *
 * The single frontend owner of the `ticket_*` command names and their request
 * envelopes (every command takes one camelCase `request` object). Features
 * consume the gateway port through DI and never call `invoke()` directly.
 *
 * Optimistic concurrency: the backend rejects a stale write with an `INVALID`
 * {@link GatewayError} whose message contains `"version conflict"`; use
 * {@link isTicketVersionConflict} to branch on it in the UI.
 */

import { Channel, invoke } from "@tauri-apps/api/core";

import type {
  GatewayError,
  ReplyChunk,
  Sprint,
  Ticket,
  TicketAttachmentContent,
  TicketBulkResult,
  TicketCarnet,
  TicketChat,
  TicketLinkKind,
  TicketList,
} from "@/domain";
import type {
  CreateTicketInput,
  TicketGateway,
  TicketListQuery,
  UpdateTicketInput,
} from "@/ports";

/**
 * Whether a gateway error is an optimistic-concurrency conflict (the ticket was
 * mutated since it was read). The backend returns a generic `INVALID` code, so
 * we key off the stable message fragment emitted by the domain store error.
 */
export function isTicketVersionConflict(error: unknown): boolean {
  if (!error || typeof error !== "object") return false;
  const message = (error as GatewayError).message;
  return typeof message === "string" && message.includes("version conflict");
}

const LEGACY_TICKET_ACTOR: Ticket["createdBy"] = { kind: "user" };

function normalizeTicketActor(actor: unknown): Ticket["createdBy"] {
  if (!actor || typeof actor !== "object") return LEGACY_TICKET_ACTOR;
  const rec = actor as Record<string, unknown>;
  if (rec.kind === "user" || rec.kind === "system") return { kind: rec.kind };
  if (rec.kind === "agent" && typeof rec.agentId === "string" && rec.agentId) {
    return { kind: "agent", agentId: rec.agentId };
  }
  return LEGACY_TICKET_ACTOR;
}

function normalizeTicket(ticket: Ticket): Ticket {
  return {
    ...ticket,
    links: Array.isArray(ticket.links) ? ticket.links : [],
    assignedAgentIds: Array.isArray(ticket.assignedAgentIds)
      ? ticket.assignedAgentIds
      : [],
    attachments: Array.isArray(ticket.attachments) ? ticket.attachments : [],
    createdBy: normalizeTicketActor(ticket.createdBy),
    updatedBy: normalizeTicketActor(ticket.updatedBy),
  };
}

function normalizeTicketList(list: TicketList): TicketList {
  return {
    ...list,
    items: Array.isArray(list.items)
      ? list.items.map((item) => ({
          ...item,
          assignedAgentIds: Array.isArray(item.assignedAgentIds)
            ? item.assignedAgentIds
            : [],
          createdBy: normalizeTicketActor(item.createdBy),
        }))
      : [],
  };
}

export class TauriTicketGateway implements TicketGateway {
  async create(projectId: string, input: CreateTicketInput): Promise<Ticket> {
    return normalizeTicket(await invoke<Ticket>("ticket_create", {
      request: { projectId, ...input },
    }));
  }

  async read(
    projectId: string,
    ref: string,
    includeCarnet = false,
  ): Promise<Ticket> {
    return normalizeTicket(await invoke<Ticket>("ticket_read", {
      request: { projectId, ref, includeCarnet },
    }));
  }

  async list(projectId: string, query?: TicketListQuery): Promise<TicketList> {
    const { statuses, priorities, ...rest } = query ?? {};
    // Multi-select facets (ticket #12): the backend expects `statuses`/
    // `priorities` arrays (empty ⇒ no constraint on that facet).
    return normalizeTicketList(await invoke<TicketList>("ticket_list", {
      request: {
        projectId,
        statuses: statuses ?? [],
        priorities: priorities ?? [],
        ...rest,
      },
    }));
  }

  async update(
    projectId: string,
    ref: string,
    input: UpdateTicketInput,
  ): Promise<Ticket> {
    return normalizeTicket(await invoke<Ticket>("ticket_update", {
      request: { projectId, ref, ...input },
    }));
  }

  async delete(projectId: string, ref: string): Promise<void> {
    // `ticket_delete` takes the shared `{ request }` envelope like every other
    // ticket command; a missing ref surfaces as a `NOT_FOUND` GatewayError from
    // the backend, propagated as-is.
    await invoke<void>("ticket_delete", {
      request: { projectId, ref },
    });
  }

  async bulkUpdateStatus(
    projectId: string,
    refs: string[],
    status: Ticket["status"],
  ): Promise<TicketBulkResult> {
    return invoke<TicketBulkResult>("ticket_bulk_update_status", {
      request: { projectId, refs, status },
    });
  }

  async bulkUpdatePriority(
    projectId: string,
    refs: string[],
    priority: Ticket["priority"],
  ): Promise<TicketBulkResult> {
    return invoke<TicketBulkResult>("ticket_bulk_update_priority", {
      request: { projectId, refs, priority },
    });
  }

  async bulkDelete(projectId: string, refs: string[]): Promise<TicketBulkResult> {
    return invoke<TicketBulkResult>("ticket_bulk_delete", {
      request: { projectId, refs },
    });
  }

  async readCarnet(projectId: string, ref: string): Promise<TicketCarnet> {
    return invoke<TicketCarnet>("ticket_read_carnet", {
      request: { projectId, ref },
    });
  }

  async updateCarnet(
    projectId: string,
    ref: string,
    carnet: string,
    expectedVersion: number,
  ): Promise<Ticket> {
    return normalizeTicket(await invoke<Ticket>("ticket_update_carnet", {
      request: { projectId, ref, carnet, expectedVersion },
    }));
  }

  async addAttachment(
    projectId: string,
    ref: string,
    path: string,
    expectedVersion: number,
    mime?: string | null,
  ): Promise<Ticket> {
    return normalizeTicket(await invoke<Ticket>("ticket_attachment_add", {
      request: { projectId, ref, path, expectedVersion, mime },
    }));
  }

  async readAttachment(
    projectId: string,
    ref: string,
    attachmentId: string,
  ): Promise<TicketAttachmentContent> {
    return invoke<TicketAttachmentContent>("ticket_attachment_read", {
      request: { projectId, ref, attachmentId },
    });
  }

  async markAttachmentSummarized(
    projectId: string,
    ref: string,
    attachmentId: string,
    expectedVersion: number,
  ): Promise<Ticket> {
    return normalizeTicket(await invoke<Ticket>("ticket_attachment_mark_summarized", {
      request: { projectId, ref, attachmentId, expectedVersion },
    }));
  }

  async link(
    projectId: string,
    ref: string,
    targetRef: string,
    kind: TicketLinkKind,
    expectedVersion: number,
  ): Promise<Ticket> {
    return normalizeTicket(await invoke<Ticket>("ticket_link", {
      request: { projectId, ref, targetRef, kind, expectedVersion },
    }));
  }

  async unlink(
    projectId: string,
    ref: string,
    targetRef: string,
    expectedVersion: number,
    kind?: TicketLinkKind,
  ): Promise<Ticket> {
    return normalizeTicket(await invoke<Ticket>("ticket_unlink", {
      request: { projectId, ref, targetRef, expectedVersion, kind },
    }));
  }

  async assign(
    projectId: string,
    ref: string,
    agentId: string,
    assigned: boolean,
    expectedVersion: number,
  ): Promise<Ticket> {
    return normalizeTicket(await invoke<Ticket>("ticket_assign", {
      request: { projectId, ref, agentId, assigned, expectedVersion },
    }));
  }

  async listSprints(projectId: string): Promise<Sprint[]> {
    // `sprint_list` returns a `SprintListDto { items }`; unwrap to the array.
    const list = await invoke<{ items: Sprint[] }>("sprint_list", {
      request: { projectId },
    });
    return list.items;
  }

  async setTicketSprint(
    projectId: string,
    ref: string,
    sprintId: string | null,
    expectedVersion: number,
  ): Promise<Ticket> {
    // Two backend commands: assign to a sprint, or unassign (clear membership).
    if (sprintId === null) {
      return normalizeTicket(await invoke<Ticket>("ticket_unassign_sprint", {
        request: { projectId, ref, expectedVersion },
      }));
    }
    return normalizeTicket(await invoke<Ticket>("ticket_assign_sprint", {
      request: { projectId, ref, sprintId, expectedVersion },
    }));
  }

  async createSprint(projectId: string, name: string): Promise<Sprint> {
    return invoke<Sprint>("sprint_create", {
      request: { projectId, name },
    });
  }

  async renameSprint(
    projectId: string,
    sprintId: string,
    name: string,
    expectedVersion: number,
  ): Promise<Sprint> {
    return invoke<Sprint>("sprint_rename", {
      request: { projectId, sprintId, name, expectedVersion },
    });
  }

  async reorderSprints(
    projectId: string,
    orderedIds: string[],
  ): Promise<Sprint[]> {
    // `sprint_reorder` returns a `SprintListDto { items }`; unwrap to the array.
    const list = await invoke<{ items: Sprint[] }>("sprint_reorder", {
      request: { projectId, orderedIds },
    });
    return list.items;
  }

  async deleteSprint(projectId: string, sprintId: string): Promise<void> {
    await invoke<void>("sprint_delete", {
      request: { projectId, sprintId },
    });
  }

  async openTicketChat(
    projectId: string,
    issueRef: string,
    profileId: string,
  ): Promise<TicketChat> {
    return invoke<TicketChat>("open_ticket_chat", {
      request: { projectId, issueRef, profileId },
    });
  }

  async closeTicketChat(projectId: string, issueRef: string): Promise<void> {
    await invoke<void>("close_ticket_chat", {
      request: { projectId, issueRef },
    });
  }

  async sendTicketChat(
    sessionId: string,
    message: string,
    onChunk: (chunk: ReplyChunk) => void,
  ): Promise<void> {
    // Per-turn reply channel, tagged on `kind` (`textDelta`|`toolActivity`|
    // `final`). Reuses the shared `agent_send` chat transport (the same command
    // and ChatBridge the structured chat cell uses). Wire the handler before the
    // invoke so no early chunk is missed.
    const channel = new Channel<ReplyChunk>();
    channel.onmessage = (chunk) => onChunk(chunk);
    await invoke<void>("agent_send", {
      sessionId,
      prompt: message,
      onReply: channel,
    });
  }
}
