/**
 * View-model hook for a single ticket's detail/edit surface (F3/F4/F5).
 *
 * Owns optimistic-concurrency handling: every mutation sends the ticket's
 * current `version` as `expectedVersion`. When the backend reports a version
 * conflict (the ticket moved underneath — e.g. an agent edited it), the hook
 * flips `conflict`, reloads the fresh ticket and surfaces a clear message so the
 * user can re-apply their change against the new version.
 *
 * The loaded ticket always includes its carnet body (F4); mutations that return
 * a carnet-less ticket preserve the last known carnet so the editor never blanks
 * out unexpectedly.
 */

import { useCallback, useEffect, useState } from "react";

import { isTicketEvent, type GatewayError, type Ticket, type TicketLinkKind } from "@/domain";
import { useGateways } from "@/app/di";

export interface TicketDetailViewModel {
  ticket: Ticket | null;
  error: string | null;
  /** Set when the last write lost an optimistic-concurrency race (F3). */
  conflict: boolean;
  /**
   * Set once the open ticket has been deleted — by this view or any other actor
   * (ticket #6). Driven by the `issueDeleted` domain event (single source of
   * truth); the surface reacts by closing itself.
   */
  deleted: boolean;
  busy: boolean;
  /**
   * Monotonic counter bumped **only** when the ticket is (re)loaded from the
   * gateway — initial load, an external-event refresh, or a conflict reload. It
   * is deliberately **not** bumped by an ordinary optimistic mutation (e.g. a
   * priority change), so a consumer can re-seed its local edit draft on genuine
   * reloads without clobbering an in-progress edit on every version bump (#9).
   */
  reloadCount: number;
  refresh: () => Promise<void>;
  updateFields: (input: {
    title?: string;
    description?: string;
    status?: Ticket["status"];
    priority?: Ticket["priority"];
  }) => Promise<boolean>;
  saveCarnet: (carnet: string) => Promise<boolean>;
  link: (targetRef: string, kind: TicketLinkKind) => Promise<boolean>;
  unlink: (targetRef: string, kind?: TicketLinkKind) => Promise<boolean>;
  assign: (agentId: string, assigned: boolean) => Promise<boolean>;
  /**
   * Deletes this ticket (ticket #6). Returns `true` on success. The removal from
   * lists and the closing of this surface flow from the resulting `issueDeleted`
   * event (see {@link TicketDetailViewModel.deleted}), not from local mutation.
   */
  remove: () => Promise<boolean>;
}

function describe(e: unknown): string {
  if (e && typeof e === "object" && "message" in e) {
    return String((e as GatewayError).message);
  }
  return String(e);
}

/** A generic `INVALID` from the backend whose message flags a version conflict. */
function isVersionConflict(e: unknown): boolean {
  return (
    !!e &&
    typeof e === "object" &&
    typeof (e as GatewayError).message === "string" &&
    (e as GatewayError).message.includes("version conflict")
  );
}

export function useTicketDetail(
  projectId: string,
  ref: string,
): TicketDetailViewModel {
  const { ticket: gateway, system } = useGateways();
  const [ticket, setTicket] = useState<Ticket | null>(null);
  const [error, setError] = useState<string | null>(null);
  const [conflict, setConflict] = useState(false);
  const [deleted, setDeleted] = useState(false);
  const [busy, setBusy] = useState(false);
  const [reloadCount, setReloadCount] = useState(0);

  const refresh = useCallback(async () => {
    setBusy(true);
    setError(null);
    try {
      setTicket(await gateway.read(projectId, ref, true));
      setConflict(false);
      // Signal a benign (re)load so consumers can refresh their edit draft; an
      // optimistic mutation below never bumps this (#9). A benign reload leaves
      // `conflict` false, so the draft is preserved field-by-field; a conflict
      // reload (see `run`) keeps `conflict` true to force a clean re-apply.
      setReloadCount((c) => c + 1);
    } catch (e) {
      setError(describe(e));
    } finally {
      setBusy(false);
    }
  }, [projectId, ref, gateway]);

  useEffect(() => {
    void refresh();
  }, [refresh]);

  // Refresh when *another* actor mutates this same ticket while it is open.
  useEffect(() => {
    if (!system) return;
    let unsubscribe: (() => void) | undefined;
    let cancelled = false;
    void system
      .onDomainEvent((event) => {
        if (event.type === "issueDeleted" && event.issueRef === ref) {
          // The open ticket was deleted (here or elsewhere): don't refresh (it
          // would 404) — flag it so the surface closes (#6).
          setDeleted(true);
          return;
        }
        if (isTicketEvent(event) && event.issueRef === ref) void refresh();
      })
      .then((u) => {
        if (cancelled) u();
        else unsubscribe = u;
      });
    return () => {
      cancelled = true;
      unsubscribe?.();
    };
  }, [refresh, system, ref]);

  /**
   * Runs one mutation, preserving the loaded carnet (mutations return a
   * carnet-less ticket) and translating a version conflict into `conflict` +
   * a reload so the UI can prompt a clean retry.
   */
  const run = useCallback(
    async (op: (version: number) => Promise<Ticket>, carnet?: string): Promise<boolean> => {
      if (!ticket) return false;
      setBusy(true);
      setError(null);
      try {
        const updated = await op(ticket.version);
        setTicket({
          ...updated,
          carnet: carnet !== undefined ? carnet : ticket.carnet,
        });
        setConflict(false);
        return true;
      } catch (e) {
        if (isVersionConflict(e)) {
          // Conflict reload: reload the fresh ticket but keep `conflict` true so
          // the consumer force-re-seeds the draft (re-apply cleanly) rather than
          // preserving the now-stale edit. Bump `reloadCount` to trigger it.
          setError(
            "This ticket was modified elsewhere. It has been reloaded — re-apply your change.",
          );
          try {
            setTicket(await gateway.read(projectId, ref, true));
          } catch (reloadErr) {
            setError(describe(reloadErr));
          }
          setConflict(true);
          setReloadCount((c) => c + 1);
        } else {
          setError(describe(e));
        }
        return false;
      } finally {
        setBusy(false);
      }
    },
    [ticket, gateway, projectId, ref],
  );

  const updateFields: TicketDetailViewModel["updateFields"] = useCallback(
    (input) => run((version) => gateway.update(projectId, ref, { ...input, expectedVersion: version })),
    [run, gateway, projectId, ref],
  );

  const saveCarnet: TicketDetailViewModel["saveCarnet"] = useCallback(
    (carnet) =>
      run((version) => gateway.updateCarnet(projectId, ref, carnet, version), carnet),
    [run, gateway, projectId, ref],
  );

  const link: TicketDetailViewModel["link"] = useCallback(
    (targetRef, kind) =>
      run((version) => gateway.link(projectId, ref, targetRef, kind, version)),
    [run, gateway, projectId, ref],
  );

  const unlink: TicketDetailViewModel["unlink"] = useCallback(
    (targetRef, kind) =>
      run((version) => gateway.unlink(projectId, ref, targetRef, version, kind)),
    [run, gateway, projectId, ref],
  );

  const assign: TicketDetailViewModel["assign"] = useCallback(
    (agentId, assigned) =>
      run((version) => gateway.assign(projectId, ref, agentId, assigned, version)),
    [run, gateway, projectId, ref],
  );

  const remove: TicketDetailViewModel["remove"] = useCallback(async () => {
    setBusy(true);
    setError(null);
    try {
      await gateway.delete(projectId, ref);
      // Closing/removal flows from the `issueDeleted` event (single source of
      // truth) — see the `deleted` flag set in the subscription above.
      return true;
    } catch (e) {
      setError(describe(e));
      return false;
    } finally {
      setBusy(false);
    }
  }, [gateway, projectId, ref]);

  return {
    ticket,
    error,
    conflict,
    deleted,
    busy,
    reloadCount,
    refresh,
    updateFields,
    saveCarnet,
    link,
    unlink,
    assign,
    remove,
  };
}
