import { useCallback, useEffect, useRef, useState } from "react";
import { AppState, type AppStateStatus } from "react-native";
import AsyncStorage from "@react-native-async-storage/async-storage";
import type { JoinResponse, SessionPreview, SessionSnapshot } from "../shared/types";
import { getApiBaseUrl, getWsUrl } from "../config/api";
import {
  buildScreenshotFixture,
  type ScreenshotFixture,
  type ScreenshotScene,
} from "../dev/screenshot-fixtures";
import { tStatic } from "../i18n";
import { registerForPushNotificationsAsync } from "../notifications";
import {
  CONNECTION_PING_INTERVAL_MS,
  CONNECTION_WATCHDOG_INTERVAL_MS,
  type SessionConnectionState,
  getReconnectDelayMs,
  isConnectionStale,
  isTerminalSocketClose,
} from "./connection";

const STORAGE_KEY = "negopoly:session";

type StoredSession = {
  sessionId: string;
  sessionCode: string;
  playerId: string;
};

type IncomingMessage =
  | { type: "state"; session: SessionSnapshot }
  | { type: "error"; message: string }
  | { type: "takeover_approved"; assignedPlayerId: string }
  | { type: "pong" };

async function readStoredSession(): Promise<StoredSession | null> {
  try {
    const raw = await AsyncStorage.getItem(STORAGE_KEY);
    if (!raw) return null;
    return JSON.parse(raw) as StoredSession;
  } catch {
    return null;
  }
}

async function writeStoredSession(session: StoredSession) {
  await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(session));
}

async function clearStoredSession() {
  await AsyncStorage.removeItem(STORAGE_KEY);
}

export function useSessionManager() {
  const [sessionId, setSessionId] = useState("");
  const [sessionCode, setSessionCode] = useState("");
  const [playerId, setPlayerId] = useState("");
  const [session, setSession] = useState<SessionSnapshot | null>(null);
  const [screenshot, setScreenshot] = useState<ScreenshotFixture | null>(null);
  const [error, setError] = useState<string | null>(null);
  const [connectionState, setConnectionState] =
    useState<SessionConnectionState>("idle");
  const [pushToken, setPushToken] = useState<{
    token: string;
    platform: "ios" | "android";
  } | null>(null);
  const [reconnectAttempt, setReconnectAttempt] = useState(0);
  const [lastActivityAt, setLastActivityAt] = useState<number | null>(null);

  const wsRef = useRef<WebSocket | null>(null);
  const sessionIdRef = useRef(sessionId);
  const sessionCodeRef = useRef(sessionCode);
  const playerIdRef = useRef(playerId);
  const sessionRef = useRef<SessionSnapshot | null>(session);
  const connectionGenerationRef = useRef(0);
  const reconnectAttemptRef = useRef(0);
  const reconnectTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  const pingTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
  const watchdogTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
  const suppressReconnectRef = useRef(false);
  const lastActivityAtRef = useRef<number | null>(null);
  const appStateRef = useRef<AppStateStatus>(AppState.currentState);
  const lastPushRegistrationRef = useRef<string | null>(null);
  const screenshotRef = useRef<ScreenshotFixture | null>(null);

  function markActivity(at = Date.now()) {
    lastActivityAtRef.current = at;
    setLastActivityAt(at);
  }

  function clearReconnectTimer() {
    if (reconnectTimerRef.current) {
      clearTimeout(reconnectTimerRef.current);
      reconnectTimerRef.current = null;
    }
  }

  function clearSocketTimers() {
    if (pingTimerRef.current) {
      clearInterval(pingTimerRef.current);
      pingTimerRef.current = null;
    }
    if (watchdogTimerRef.current) {
      clearInterval(watchdogTimerRef.current);
      watchdogTimerRef.current = null;
    }
  }

  function closeSocket(ws: WebSocket | null, code?: number, reason?: string) {
    if (!ws || ws.readyState === WebSocket.CLOSED) {
      return;
    }
    try {
      if (typeof code === "number") {
        ws.close(code, reason);
        return;
      }
      ws.close();
    } catch {
      // Ignore close failures.
    }
  }

  function teardownConnection() {
    clearReconnectTimer();
    clearSocketTimers();
    connectionGenerationRef.current += 1;
    const ws = wsRef.current;
    wsRef.current = null;
    closeSocket(ws);
  }

  function scheduleReconnect(generation: number) {
    if (
      suppressReconnectRef.current ||
      generation !== connectionGenerationRef.current ||
      !sessionIdRef.current ||
      !playerIdRef.current
    ) {
      return;
    }

    clearReconnectTimer();
    const nextAttempt = reconnectAttemptRef.current + 1;
    reconnectAttemptRef.current = nextAttempt;
    setReconnectAttempt(nextAttempt);
    setConnectionState("reconnecting");

    const delay = getReconnectDelayMs(nextAttempt - 1);
    reconnectTimerRef.current = setTimeout(() => {
      reconnectTimerRef.current = null;
      openSocket("retry");
    }, delay);
  }

  function startSocketTimers(ws: WebSocket, generation: number) {
    clearSocketTimers();

    pingTimerRef.current = setInterval(() => {
      if (
        generation !== connectionGenerationRef.current ||
        ws.readyState !== WebSocket.OPEN
      ) {
        return;
      }
      try {
        ws.send(
          JSON.stringify({
            type: "ping",
            sessionId: sessionIdRef.current,
            playerId: playerIdRef.current,
          }),
        );
      } catch {
        closeSocket(ws, 4001, "Ping failed");
      }
    }, CONNECTION_PING_INTERVAL_MS);

    watchdogTimerRef.current = setInterval(() => {
      if (
        generation !== connectionGenerationRef.current ||
        appStateRef.current !== "active"
      ) {
        return;
      }
      if (!isConnectionStale(lastActivityAtRef.current)) {
        return;
      }
      if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
        closeSocket(ws, 4000, "Connection stale");
        return;
      }
      scheduleReconnect(generation);
    }, CONNECTION_WATCHDOG_INTERVAL_MS);
  }

  function openSocket(reason: "initial" | "retry" | "resume" | "manual") {
    const targetSessionId = sessionIdRef.current;
    const targetPlayerId = playerIdRef.current;
    if (!targetSessionId || !targetPlayerId) {
      return;
    }

    clearReconnectTimer();
    clearSocketTimers();

    const previousSocket = wsRef.current;
    const generation = connectionGenerationRef.current + 1;
    connectionGenerationRef.current = generation;
    wsRef.current = null;
    closeSocket(previousSocket);

    const recovering = Boolean(sessionRef.current) || reason !== "initial";
    setConnectionState(recovering ? "reconnecting" : "connecting");

    const ws = new WebSocket(getWsUrl(targetSessionId, targetPlayerId));
    wsRef.current = ws;

    ws.onopen = () => {
      if (connectionGenerationRef.current !== generation) {
        return;
      }
      reconnectAttemptRef.current = 0;
      setReconnectAttempt(0);
      setConnectionState("open");
      setError(null);
      markActivity();
      startSocketTimers(ws, generation);
    };

    ws.onmessage = (event) => {
      if (connectionGenerationRef.current !== generation) {
        return;
      }

      markActivity();

      try {
        const message = JSON.parse(event.data) as IncomingMessage;
        if (message.type === "state") {
          setSession(message.session);
          return;
        }
        if (message.type === "error") {
          setError(message.message);
          return;
        }
        if (message.type === "takeover_approved") {
          const assignedId = message.assignedPlayerId;
          setPlayerId(assignedId);
          if (sessionIdRef.current && sessionCodeRef.current) {
            void writeStoredSession({
              sessionId: sessionIdRef.current,
              sessionCode: sessionCodeRef.current,
              playerId: assignedId,
            });
          }
        }
      } catch {
        setError(tStatic("error.parseResponse"));
      }
    };

    ws.onerror = () => {
      if (connectionGenerationRef.current !== generation) {
        return;
      }
      setConnectionState("reconnecting");
    };

    ws.onclose = (event) => {
      if (connectionGenerationRef.current !== generation) {
        return;
      }

      if (wsRef.current === ws) {
        wsRef.current = null;
      }
      clearSocketTimers();

      const reasonText = typeof event?.reason === "string" ? event.reason : "";
      if (isTerminalSocketClose(event?.code, reasonText)) {
        setConnectionState("error");
        void resetSession();
        return;
      }

      if (
        suppressReconnectRef.current ||
        !sessionIdRef.current ||
        !playerIdRef.current
      ) {
        setConnectionState("idle");
        return;
      }

      scheduleReconnect(generation);
    };
  }

  function retryConnection() {
    if (!sessionIdRef.current || !playerIdRef.current) {
      return;
    }
    suppressReconnectRef.current = false;
    reconnectAttemptRef.current = 0;
    setReconnectAttempt(0);
    openSocket("manual");
  }

  useEffect(() => {
    sessionIdRef.current = sessionId;
    sessionCodeRef.current = sessionCode;
    playerIdRef.current = playerId;
  }, [playerId, sessionCode, sessionId]);

  useEffect(() => {
    screenshotRef.current = screenshot;
  }, [screenshot]);

  useEffect(() => {
    sessionRef.current = session;
  }, [session]);

  useEffect(() => {
    let mounted = true;
    readStoredSession().then((stored) => {
      if (!mounted || !stored || screenshotRef.current) return;
      setSessionId(stored.sessionId);
      setSessionCode(stored.sessionCode);
      setPlayerId(stored.playerId);
    });
    return () => {
      mounted = false;
    };
  }, []);

  useEffect(() => {
    if (screenshot) return;
    let mounted = true;
    registerForPushNotificationsAsync().then((token) => {
      if (!mounted) return;
      setPushToken(token);
    });
    return () => {
      mounted = false;
    };
  }, [screenshot]);

  useEffect(() => {
    if (screenshot) return;
    const subscription = AppState.addEventListener("change", (nextState) => {
      const previousState = appStateRef.current;
      appStateRef.current = nextState;

      if (
        previousState !== "active" &&
        nextState === "active" &&
        sessionIdRef.current &&
        playerIdRef.current
      ) {
        const socket = wsRef.current;
        const socketOpen = socket?.readyState === WebSocket.OPEN;
        if (!socketOpen || isConnectionStale(lastActivityAtRef.current)) {
          retryConnection();
          return;
        }
        try {
          socket.send(
            JSON.stringify({
              type: "ping",
              sessionId: sessionIdRef.current,
              playerId: playerIdRef.current,
            }),
          );
        } catch {
          retryConnection();
        }
      }
    });

    return () => {
      subscription.remove();
    };
  }, [screenshot]);

  useEffect(() => {
    if (screenshot) return;
    if (!pushToken || !sessionId || !playerId) return;
    void registerPushTokenFor(sessionId, playerId);
  }, [pushToken, screenshot, sessionId, playerId]);

  useEffect(() => {
    if (screenshot) {
      suppressReconnectRef.current = true;
      teardownConnection();
      reconnectAttemptRef.current = 0;
      setReconnectAttempt(0);
      lastActivityAtRef.current = null;
      setLastActivityAt(null);
      setConnectionState("idle");
      return;
    }
    if (!sessionId || !playerId) {
      suppressReconnectRef.current = true;
      teardownConnection();
      reconnectAttemptRef.current = 0;
      setReconnectAttempt(0);
      lastActivityAtRef.current = null;
      setLastActivityAt(null);
      setConnectionState("idle");
      setSession(null);
      return;
    }

    suppressReconnectRef.current = false;
    reconnectAttemptRef.current = 0;
    setReconnectAttempt(0);
    openSocket("initial");

    return () => {
      teardownConnection();
    };
  }, [playerId, screenshot, sessionId]);

  const activateScreenshotScene = useCallback((scene: ScreenshotScene) => {
    const fixture = buildScreenshotFixture(scene);
    suppressReconnectRef.current = true;
    teardownConnection();
    reconnectAttemptRef.current = 0;
    setReconnectAttempt(0);
    lastActivityAtRef.current = null;
    setLastActivityAt(null);
    setConnectionState("idle");
    setError(null);
    setScreenshot(fixture);
    setSessionId(fixture.sessionId);
    setSessionCode(fixture.sessionCode);
    setPlayerId(fixture.playerId);
    setSession(fixture.session);
  }, []);

  async function registerPushTokenFor(targetSessionId: string, targetPlayerId: string) {
    if (!pushToken) return;
    const signature = `${targetSessionId}:${targetPlayerId}:${pushToken.platform}:${pushToken.token}`;
    if (lastPushRegistrationRef.current === signature) return;
    lastPushRegistrationRef.current = signature;
    try {
      await fetch(`${getApiBaseUrl()}/api/push/register`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          sessionId: targetSessionId,
          playerId: targetPlayerId,
          token: pushToken.token,
          platform: pushToken.platform,
        }),
      });
    } catch {
      // Ignore push registration failures.
    }
  }

  async function requestTakeover(
    dummyId: string,
    overrideSessionId?: string,
    overridePlayerId?: string,
  ): Promise<string | null> {
    const targetSessionId = overrideSessionId ?? sessionId;
    const targetPlayerId = overridePlayerId ?? playerId;
    if (!targetSessionId || !targetPlayerId) {
      const message = tStatic("entry.alert.takeoverFailed");
      setError(message);
      return message;
    }
    const payload = {
      type: "takeover_request",
      sessionId: targetSessionId,
      playerId: targetPlayerId,
      dummyId,
    };
    if (
      wsRef.current &&
      wsRef.current.readyState === WebSocket.OPEN &&
      targetSessionId === sessionId &&
      targetPlayerId === playerId
    ) {
      wsRef.current.send(JSON.stringify(payload));
      return null;
    }
    try {
      const response = await fetch(
        `${getApiBaseUrl()}/api/session/${targetSessionId}/takeover`,
        {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ playerId: targetPlayerId, dummyId }),
        },
      );
      if (!response.ok) {
        const data = (await response.json()) as { message?: string };
        const message = data.message ?? tStatic("entry.alert.takeoverFailed");
        setError(message);
        return message;
      }
      return null;
    } catch {
      const message = tStatic("error.connectionNotReady");
      setError(message);
      return message;
    }
  }

  async function createSession(bankerName: string) {
    setError(null);
    setScreenshot(null);
    setSessionId("");
    setSessionCode("");
    setPlayerId("");
    setSession(null);
    try {
      const response = await fetch(`${getApiBaseUrl()}/api/session`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ bankerName }),
      });
      if (!response.ok) {
        setError(tStatic("error.createSession"));
        return null;
      }
      const data = (await response.json()) as JoinResponse;
      setSessionId(data.sessionId);
      setSessionCode(data.sessionCode);
      setPlayerId(data.playerId);
      await writeStoredSession({
        sessionId: data.sessionId,
        sessionCode: data.sessionCode,
        playerId: data.playerId,
      });
      return data;
    } catch {
      setError(tStatic("error.createSession"));
      return null;
    }
  }

  async function joinSession(code: string, name: string) {
    setError(null);
    setScreenshot(null);
    setSessionId("");
    setSessionCode("");
    setPlayerId("");
    setSession(null);
    if (!code) {
      setError(tStatic("entry.alert.enterCode"));
      return null;
    }
    const storedNow = await readStoredSession();
    const reusePlayerId =
      storedNow && (storedNow.sessionCode === code || storedNow.sessionId === code)
        ? storedNow.playerId
        : undefined;

    try {
      const response = await fetch(`${getApiBaseUrl()}/api/session/${code}/join`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ name, playerId: reusePlayerId }),
      });
      if (!response.ok) {
        setError(tStatic("error.joinSession"));
        return null;
      }
      const data = (await response.json()) as JoinResponse;
      setSessionId(data.sessionId);
      setSessionCode(data.sessionCode);
      setPlayerId(data.playerId);
      await writeStoredSession({
        sessionId: data.sessionId,
        sessionCode: data.sessionCode,
        playerId: data.playerId,
      });
      return data;
    } catch {
      setError(tStatic("error.joinSession"));
      return null;
    }
  }

  async function requestTakeoverToken(
    code: string,
    dummyId: string,
    name: string,
  ): Promise<string | null> {
    setError(null);
    if (!code || !dummyId) {
      setError(tStatic("entry.alert.selectDummy"));
      return null;
    }
    try {
      const response = await fetch(
        `${getApiBaseUrl()}/api/session/${code}/takeover-request`,
        {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ dummyId, name }),
        },
      );
      if (!response.ok) {
        const data = (await response.json()) as { message?: string };
        setError(data.message ?? tStatic("entry.alert.takeoverFailed"));
        return null;
      }
      const data = (await response.json()) as { token?: string };
      return data.token ?? null;
    } catch {
      setError(tStatic("error.connectionNotReady"));
      return null;
    }
  }

  async function claimTakeover(code: string, token: string) {
    try {
      setScreenshot(null);
      setSessionId("");
      setSessionCode("");
      setPlayerId("");
      const response = await fetch(
        `${getApiBaseUrl()}/api/session/${code}/takeover-claim`,
        {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ token }),
        },
      );
      if (response.status === 409) {
        return null;
      }
      if (!response.ok) {
        const data = (await response.json()) as { message?: string };
        setError(data.message ?? tStatic("entry.alert.takeoverFailed"));
        return null;
      }
      const data = (await response.json()) as JoinResponse;
      setSessionId(data.sessionId);
      setSessionCode(data.sessionCode);
      setPlayerId(data.playerId);
      await writeStoredSession({
        sessionId: data.sessionId,
        sessionCode: data.sessionCode,
        playerId: data.playerId,
      });
      await registerPushTokenFor(data.sessionId, data.playerId);
      return data;
    } catch {
      setError(tStatic("error.connectionNotReady"));
      return null;
    }
  }

  async function fetchSessionPreview(code: string): Promise<SessionPreview | null> {
    if (!code) return null;
    try {
      const response = await fetch(`${getApiBaseUrl()}/api/session/${code}/info`);
      if (!response.ok) {
        setError(tStatic("error.loadSessionInfo"));
        return null;
      }
      return (await response.json()) as SessionPreview;
    } catch {
      setError(tStatic("error.loadSessionInfo"));
      return null;
    }
  }

  function sendMessage(payload: Record<string, unknown>) {
    if (screenshotRef.current) {
      return;
    }
    if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) {
      retryConnection();
      setError(
        connectionState === "reconnecting" || connectionState === "connecting"
          ? tStatic("error.reconnecting")
          : tStatic("error.connectionNotReady"),
      );
      return;
    }
    wsRef.current.send(JSON.stringify(payload));
  }

  async function resetSession() {
    suppressReconnectRef.current = true;
    teardownConnection();
    reconnectAttemptRef.current = 0;
    setReconnectAttempt(0);
    lastActivityAtRef.current = null;
    setLastActivityAt(null);
    try {
      await clearStoredSession();
    } catch {
      // Ignore storage errors on reset.
    }
    setSessionId("");
    setSessionCode("");
    setPlayerId("");
    setSession(null);
    setScreenshot(null);
    setError(null);
    setConnectionState("idle");
  }

  async function leaveSession() {
    suppressReconnectRef.current = true;
    teardownConnection();
    await resetSession();
  }

  const me = session?.players.find((player) => player.id === playerId) ?? null;
  const isBanker = me?.role === "banker";

  return {
    sessionId,
    sessionCode,
    playerId,
    session,
    me,
    isBanker,
    error,
    connectionState,
    reconnectAttempt,
    lastActivityAt,
    setError,
    createSession,
    joinSession,
    fetchSessionPreview,
    requestTakeoverToken,
    claimTakeover,
    sendMessage,
    resetSession,
    leaveSession,
    retryConnection,
    setSessionId,
    setPlayerId,
    setSession,
    requestTakeover,
    activateScreenshotScene,
    screenshot,
  };
}
