import { useSessionStore } from '@/stores/sessionStore';
import { usePlayerSessionStore } from '@/stores/playerSessionStore';
import { useUiStore } from '@/stores/uiStore';
import { charactersRepo, sessionLogRepo } from '@/lib/db/repositories';
import type { Character } from '@/lib/schemas';
import { clientMessageSchema, serverMessageSchema, type ClientMessage, type Snapshot, type PartialCharacterDiff } from './messages';

/** GM-only: persist a roll/chat line to the campaign's session recap. */
function persistLog(kind: 'roll' | 'chat', from: string, text: string, scope?: 'table' | 'whisper'): void {
  if (useSessionStore.getState().role !== 'gm') return;
  const campaignId = useUiStore.getState().activeCampaignId;
  if (!campaignId) return;
  void sessionLogRepo.add({ campaignId, kind, from, text, ...(scope ? { scope } : {}) });
}

/**
 * WebSocket transport for live sessions. The GM hosts and pushes player-safe
 * snapshots; players join read-only and render received state. Reconnects with
 * backoff and re-hosts/re-joins automatically. No-op-safe when offline.
 */

let ws: WebSocket | null = null;
let gmSecret = '';
let intent: { kind: 'gm'; campaignId: string; password?: string } | { kind: 'player'; joinCode: string; password?: string } | null = null;
let lastSnapshot: Snapshot | null = null;
/** images sent this session (map background + token icons / portraits), by id */
const sentImages = new Map<string, string>();
let reconnectMs = 1000;
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
let manualStop = false;

function wsUrl(): string {
  const proto = location.protocol === 'https:' ? 'wss' : 'ws';
  return `${proto}://${location.host}/ws`;
}

const PLAYER_ID_KEY = 'ttrpg:playerId';
/** A stable per-browser id so a transient WebSocket drop keeps the player's seat. */
function stablePlayerId(): string {
  try {
    let id = localStorage.getItem(PLAYER_ID_KEY);
    if (!id) { id = crypto.randomUUID(); localStorage.setItem(PLAYER_ID_KEY, id); }
    return id;
  } catch {
    return crypto.randomUUID();
  }
}

function send(msg: ClientMessage): void {
  if (ws && ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(clientMessageSchema.parse(msg)));
}

function connect(): void {
  if (!intent) return;
  useSessionStore.getState().set({ status: 'connecting', error: null });
  let socket: WebSocket;
  try {
    socket = new WebSocket(wsUrl());
  } catch {
    useSessionStore.getState().set({ status: 'error', error: 'Could not open connection.' });
    return;
  }
  ws = socket;

  socket.onopen = () => {
    reconnectMs = 1000;
    if (!intent) return;
    if (intent.kind === 'gm') send({ t: 'host', campaignId: intent.campaignId, ...(intent.password ? { password: intent.password } : {}), ...(gmSecret ? { resume: gmSecret } : {}) });
    else send({ t: 'join', joinCode: intent.joinCode, ...(intent.password ? { password: intent.password } : {}), playerId: stablePlayerId() });
  };

  socket.onmessage = (ev) => {
    let parsed;
    try { parsed = serverMessageSchema.safeParse(JSON.parse(String(ev.data))); } catch { return; }
    if (!parsed.success) return;
    const msg = parsed.data;
    const session = useSessionStore.getState();
    if (msg.t === 'hosted') {
      gmSecret = msg.gmSecret;
      session.set({ role: 'gm', status: 'connected', roomId: msg.roomId, joinCode: msg.joinCode, error: null });
      // (re)send current state on (re)connect
      if (lastSnapshot) send({ t: 'state', gmSecret, snapshot: lastSnapshot });
      for (const [id, dataUrl] of sentImages) send({ t: 'image', gmSecret, id, dataUrl });
    } else if (msg.t === 'joined') {
      session.set({ role: 'player', status: 'connected', roomId: msg.roomId, error: null });
      const guestName = useSessionStore.getState().guestName.trim();
      if (guestName) send({ t: 'setName', name: guestName });
    } else if (msg.t === 'snapshot') {
      usePlayerSessionStore.getState().setSnapshot(msg.snapshot);
      // fetch the map image if we don't have it cached
      const id = msg.snapshot.mapImageId;
      if (id && !usePlayerSessionStore.getState().images[id]) send({ t: 'requestImage', id });
    } else if (msg.t === 'mapImage') {
      usePlayerSessionStore.getState().setImage(msg.id, msg.dataUrl);
    } else if (msg.t === 'seatRequest') {
      if (session.role === 'gm') useSessionStore.getState().addSeatRequest({ playerId: msg.playerId, characterId: msg.characterId, ...(msg.offlineSnapshot ? { offlineSnapshot: msg.offlineSnapshot } : {}) });
    } else if (msg.t === 'seatGranted') {
      usePlayerSessionStore.getState().setMyCharacter(msg.character);
    } else if (msg.t === 'playerPatched') {
      if (session.role === 'gm') {
        const d = msg.diff;
        void charactersRepo.update(msg.characterId, {
          ...(d.hp ? { hp: d.hp } : {}),
          ...(d.conditions ? { conditions: d.conditions } : {}),
          ...(d.spellcasting ? { spellcasting: d.spellcasting } : {}),
          ...(d.resources ? { resources: d.resources } : {}),
          ...(d.defenses ? { defenses: d.defenses } : {}),
          ...(d.concentration !== undefined ? { concentration: d.concentration } : {}),
        });
      }
    } else if (msg.t === 'rollBroadcast') {
      usePlayerSessionStore.getState().addRoll({ playerName: msg.playerName, label: msg.label, expression: msg.expression, total: msg.total, breakdown: msg.breakdown });
      persistLog('roll', msg.playerName, `${msg.label || msg.expression} → ${msg.total}`);
    } else if (msg.t === 'roster') {
      useSessionStore.getState().setRoster(msg.players);
    } else if (msg.t === 'privateHandout') {
      usePlayerSessionStore.getState().setPrivateHandout(msg.handout);
    } else if (msg.t === 'chat') {
      usePlayerSessionStore.getState().addChat({ from: msg.from, body: msg.body, scope: msg.scope, ...(msg.to ? { to: msg.to } : {}) });
      persistLog('chat', msg.from, msg.body, msg.scope);
    } else if (msg.t === 'error') {
      session.set({ status: 'error', error: msg.message });
      if (msg.code === 'no-room' || msg.code === 'bad-password') manualStop = true;
    }
  };

  socket.onclose = () => {
    ws = null;
    if (manualStop || !intent) return;
    useSessionStore.getState().set({ status: 'connecting' });
    reconnectTimer = setTimeout(connect, reconnectMs);
    reconnectMs = Math.min(15000, reconnectMs * 2);
  };
  socket.onerror = () => { try { socket.close(); } catch { /* ignore */ } };
}

export function hostSession(campaignId: string, password?: string): void {
  manualStop = false;
  intent = { kind: 'gm', campaignId, ...(password ? { password } : {}) };
  connect();
}

export function joinSession(joinCode: string, password?: string): void {
  manualStop = false;
  intent = { kind: 'player', joinCode: joinCode.trim().toUpperCase(), ...(password ? { password } : {}) };
  usePlayerSessionStore.getState().reset();
  connect();
}

export function pushSnapshot(snapshot: Snapshot): void {
  lastSnapshot = snapshot;
  if (useSessionStore.getState().role === 'gm') send({ t: 'state', gmSecret, snapshot });
}

export function pushImage(id: string, dataUrl: string): void {
  if (sentImages.get(id) === dataUrl) return; // already sent (dedup by content)
  sentImages.set(id, dataUrl);
  if (useSessionStore.getState().role === 'gm') send({ t: 'image', gmSecret, id, dataUrl });
}

// ---- two-way play ----

/** Player: request to control a character (optionally bundling offline edits). */
export function claimSeat(characterId: string, offlineSnapshot?: Character): void {
  usePlayerSessionStore.getState().setSeatStatus('pending');
  send({ t: 'claimSeat', characterId, ...(offlineSnapshot ? { offlineSnapshot } : {}) });
}

/** Player: push a live edit to their own sheet (GM persists it). */
export function sendPlayerPatch(characterId: string, diff: PartialCharacterDiff): void {
  send({ t: 'playerPatch', characterId, diff });
}

/** Player: broadcast a dice roll to the whole table. */
export function sendPlayerRoll(characterId: string, label: string, expression: string, total: number, breakdown: string): void {
  send({ t: 'playerRoll', characterId, label, expression, total, breakdown });
}

/** GM: broadcast one of their own (public) rolls to the players. No-op unless hosting. */
export function broadcastGmRoll(label: string, expression: string, total: number, breakdown: string): void {
  if (useSessionStore.getState().role === 'gm' && gmSecret) {
    send({ t: 'gmRoll', gmSecret, label, expression, total, breakdown });
    // mirror into the local table feed + recap so the GM's history includes their own public rolls
    usePlayerSessionStore.getState().addRoll({ playerName: 'GM', label, expression, total, breakdown });
    persistLog('roll', 'GM', `${label || expression} → ${total}`);
  }
}

/** Player: set/update the display name shown in the roster + chat. */
export function sendSetName(name: string): void {
  if (useSessionStore.getState().role === 'player') send({ t: 'setName', name });
}

/** Send a chat message. `to` set = whisper (GM → that playerId; player → the GM). */
export function sendChat(body: string, to?: string): void {
  const text = body.trim();
  if (!text) return;
  send({ t: 'chat', body: text, ...(to ? { to } : {}) });
  const scope = to ? 'whisper' : 'table';
  // optimistic local echo (the server doesn't echo to the sender)
  usePlayerSessionStore.getState().addChat({ from: 'You', body: text, scope, mine: true, ...(to ? { to } : {}) });
  persistLog('chat', 'GM', text, scope);
}

/** GM: send a private handout/note/image to one player (or null to clear it). */
export function sendPrivateHandout(targetPlayerId: string, handout: { title: string; body: string; image?: string } | null): void {
  if (useSessionStore.getState().role === 'gm' && gmSecret) send({ t: 'privateState', gmSecret, targetPlayerId, handout });
}

/** GM: approve a seat request, handing the player their authoritative sheet. */
export function grantSeat(targetPlayerId: string, character: Character): void {
  if (useSessionStore.getState().role === 'gm') {
    send({ t: 'seatGrant', gmSecret, targetPlayerId, character });
    useSessionStore.getState().removeSeatRequest(targetPlayerId);
  }
}

/** Explicitly leave a player session: clear the persisted intent + disconnect. */
export function leaveRoom(): void {
  useSessionStore.getState().setJoinIntent(null);
  stopSession();
}

export function stopSession(): void {
  manualStop = true;
  intent = null;
  gmSecret = '';
  lastSnapshot = null;
  sentImages.clear();
  if (reconnectTimer) clearTimeout(reconnectTimer);
  try { ws?.close(); } catch { /* ignore */ }
  ws = null;
  useSessionStore.getState().reset();
  usePlayerSessionStore.getState().reset();
}
