'use client';

import { useCallback, useEffect, useRef, useState } from 'react';
import type { PowerUpType } from '@pintrush/engine';
import type { RoomJoinResult, TeamSetResult } from '@pintrush/protocol';
import { getDeviceId, saveToken, type GameConnection, type GameViewState } from '@/lib/game';
import { KeyRing, recallNickname, rememberNickname, vibrate } from '@/lib/play/device';
import { friendlyError } from '@/lib/play/errors';

export interface Notice {
  id: number;
  text: string;
  tone: 'error' | 'info' | 'success';
}

export interface ActionResult {
  ok: boolean;
  error?: string;
}

interface LocalAnswer {
  questionId: string;
  optionId: string;
  status: 'sending' | 'sent' | 'failed';
}

/**
 * Every player action, with the guarantees a bar's Wi-Fi demands: one idempotency key per
 * logical action (retries and double taps can never double-submit), optimistic answer state that
 * rolls back on a definite failure, and server views as the final truth.
 */
export function usePlayController(conn: GameConnection | null, state: GameViewState, roomId: string) {
  const keys = useRef(new KeyRing());
  const inFlight = useRef<string | null>(null);
  const powerBusyRef = useRef(false);
  const noticeSeq = useRef(0);

  const [answer, setAnswer] = useState<LocalAnswer | null>(null);
  const [wildcard, setWildcard] = useState<{ questionId: string; position: number } | null>(null);
  const [removedOption, setRemovedOption] = useState<{ questionId: string; optionId: string } | null>(null);
  const [powerBusy, setPowerBusy] = useState<PowerUpType | null>(null);
  const [swap, setSwap] = useState<number[] | null>(null);
  const [joining, setJoining] = useState(false);
  const [notice, setNotice] = useState<Notice | null>(null);

  const pub = state.public;
  const me = state.me;
  const question = pub?.question ?? null;
  const phase = pub?.round?.phase ?? null;

  const notify = useCallback((text: string, tone: Notice['tone'] = 'error') => {
    noticeSeq.current += 1;
    setNotice({ id: noticeSeq.current, text, tone });
  }, []);

  useEffect(() => {
    if (!notice) return;
    const timer = setTimeout(() => setNotice((n) => (n?.id === notice.id ? null : n)), 4_500);
    return () => clearTimeout(timer);
  }, [notice]);

  // Category Swap only makes sense between questions; drop a half-made selection when that window closes.
  const swapAllowed = me?.powerUps.available.includes('CATEGORY_SWAP') ?? false;
  useEffect(() => {
    if (!swapAllowed) setSwap(null);
  }, [swapAllowed]);

  // The answer this phone shows as locked in: the server's record wins over the optimistic one.
  const serverPick = me?.currentAnswer && question && me.currentAnswer.questionId === question.id ? me.currentAnswer.optionId : null;
  const localPick = answer && question && answer.questionId === question.id && answer.status !== 'failed' ? answer.optionId : null;
  const pick = serverPick ?? localPick;
  const sending = !serverPick && answer?.status === 'sending' && answer.questionId === question?.id;

  // Second Chance: the server's view (personal update) or, until it lands, the ack's option id.
  const removedOptionId: string | null =
    me?.powerUps.removedOptionId ?? (removedOption && question && removedOption.questionId === question.id ? removedOption.optionId : null);

  const wildcardPick = wildcard && question && wildcard.questionId === question.id ? wildcard.position : null;

  const submitAnswer = useCallback(
    async (optionId: string) => {
      if (!conn || !question || phase !== 'OPEN' || question.options === null) return;
      if (pick || inFlight.current === question.id) return; // one tap commits; ignore double taps
      const questionId = question.id;
      inFlight.current = questionId;
      setAnswer({ questionId, optionId, status: 'sending' });
      vibrate(12);
      const payload: Record<string, unknown> = { questionId, optionId, idempotencyKey: keys.current.for(`answer:${questionId}`) };
      if (question.isWildcard) payload.wildcardPosition = wildcardPick;
      const res = await conn.emitWithRetry('answer.submit', payload);
      inFlight.current = null;
      if (res.ok || res.error.code === 'ALREADY_ANSWERED') {
        setAnswer((cur) => (cur?.questionId === questionId ? { ...cur, status: 'sent' } : cur));
      } else {
        setAnswer((cur) => (cur?.questionId === questionId ? { ...cur, status: 'failed' } : cur));
        notify(friendlyError(res.error));
      }
    },
    [conn, question, phase, pick, wildcardPick, notify],
  );

  const chooseWildcard = useCallback(
    (position: number | null) => {
      if (!question?.isWildcard) return;
      setWildcard(position === null ? null : { questionId: question.id, position });
    },
    [question],
  );

  const activatePowerUp = useCallback(
    async (type: PowerUpType, extra: { questionId?: string; from?: number; to?: number } = {}): Promise<boolean> => {
      if (!conn || powerBusyRef.current) return false;
      powerBusyRef.current = true;
      setPowerBusy(type);
      // A fresh key per tap; emitWithRetry resends the same payload (same key) on flaky Wi-Fi.
      const res = await conn.emitWithRetry<{ removedOptionId?: string }>('powerup.activate', {
        type,
        ...extra,
        idempotencyKey: crypto.randomUUID(),
      });
      powerBusyRef.current = false;
      setPowerBusy(null);
      if (!res.ok) {
        notify(friendlyError(res.error));
        return false;
      }
      vibrate([10, 40, 10]);
      if (type === 'SECOND_CHANCE' && extra.questionId && res.data?.removedOptionId) {
        setRemovedOption({ questionId: extra.questionId, optionId: res.data.removedOptionId });
      }
      if (type === 'DOUBLE_DOWN') notify('Double Down armed: +200 if you’re right, −100 if not.', 'success');
      if (type === 'CATEGORY_SWAP') notify('Squares swapped.', 'success');
      return true;
    },
    [conn, notify],
  );

  const doubleDown = useCallback(
    () => activatePowerUp('DOUBLE_DOWN', phase === 'PENDING' && question ? { questionId: question.id } : {}),
    [activatePowerUp, phase, question],
  );

  const secondChance = useCallback(() => {
    if (!question || phase !== 'OPEN') return Promise.resolve(false);
    return activatePowerUp('SECOND_CHANCE', { questionId: question.id });
  }, [activatePowerUp, phase, question]);

  const confirmSwap = useCallback(async () => {
    if (!swap || swap.length !== 2) return;
    const [from, to] = swap as [number, number];
    if (await activatePowerUp('CATEGORY_SWAP', { from, to })) setSwap(null);
  }, [activatePowerUp, swap]);

  const setTeam = useCallback(
    async (teamName: string | null): Promise<ActionResult> => {
      if (!conn) return { ok: false, error: 'Reconnecting… try again in a moment.' };
      const res = await conn.emitWithRetry<TeamSetResult>('team.set', { teamName, idempotencyKey: crypto.randomUUID() });
      if (!res.ok) return { ok: false, error: friendlyError(res.error) };
      notify(res.data?.teamName ? `You’re playing for ${res.data.teamName}.` : 'You’re playing solo.', 'success');
      return { ok: true };
    },
    [conn, notify],
  );

  const predict = useCallback(
    async (finalistId: string): Promise<ActionResult> => {
      if (!conn) return { ok: false, error: 'Reconnecting…' };
      const res = await conn.emitWithRetry('final.predict', { finalistId, idempotencyKey: crypto.randomUUID() });
      if (!res.ok) {
        notify(friendlyError(res.error));
        return { ok: false, error: friendlyError(res.error) };
      }
      vibrate(12);
      return { ok: true };
    },
    [conn, notify],
  );

  /**
   * One-tap "Join the Championship": room.join on this same socket with the current nickname (the
   * server de-duplicates names). The socket becomes the championship player immediately and the
   * server pushes a fresh room.state.
   */
  const joinChampionship = useCallback(async (): Promise<ActionResult> => {
    const snap = state.snapshot;
    if (!conn || !snap) return { ok: false, error: 'Reconnecting…' };
    const nickname = state.me?.nickname ?? recallNickname(roomId);
    if (!nickname) return { ok: false, error: 'Pick a nickname on the join screen first.' };
    setJoining(true);
    const res = await conn.emitWithRetry<RoomJoinResult>('room.join', {
      venueCode: snap.venue.joinCode,
      roomId,
      nickname,
      teamName: state.me?.teamName ?? null,
      deviceId: getDeviceId(),
      idempotencyKey: keys.current.for(`join:${snap.activeSessionId ?? 'none'}`),
    });
    setJoining(false);
    if (!res.ok || !res.data) {
      const error = res.ok ? 'Could not join — try again.' : friendlyError(res.error);
      notify(error);
      return { ok: false, error };
    }
    const d = res.data;
    saveToken(roomId, {
      token: d.token,
      gameSessionId: d.gameSessionId,
      playerSessionId: d.playerSessionId,
      expiresAt: d.expiresAt,
      nickname: d.nickname,
    });
    rememberNickname(roomId, d.nickname);
    vibrate([15, 60, 15]);
    return { ok: true };
  }, [conn, roomId, state.snapshot, state.me, notify]);

  return {
    pick,
    sending,
    removedOptionId,
    wildcardPick,
    submitAnswer,
    chooseWildcard,
    powerBusy,
    doubleDown,
    secondChance,
    swap,
    setSwap,
    confirmSwap,
    setTeam,
    predict,
    joining,
    joinChampionship,
    notice,
    notify,
    dismissNotice: () => setNotice(null),
  };
}

export type PlayController = ReturnType<typeof usePlayController>;
