import {
  Client,
  Session,
  Socket,
  MatchmakerTicket,
  MatchData,
  MatchmakerMatched,
} from "@heroiclabs/nakama-js";

import {
  ApiMatch,
  ApiLeaderboardRecordList,
// @ts-ignore
} from "@heroiclabs/nakama-js/dist/api.gen"

import React, { createContext, useContext, useState } from "react";

function getOrCreateDeviceId(): string {
  const key = "nakama.deviceId";
  let id = localStorage.getItem(key);
  if (!id) {
    id = crypto.randomUUID();
    localStorage.setItem(key, id);
  }
  return id;
}

export interface NakamaContextType {
  client: Client;
  socket: Socket | null;
  session: Session | null;
  matchId: string | null;

  loginOrRegister(username: string): Promise<void>;
  logout(): Promise<void>;
  joinMatchmaker(mode: string): Promise<string>;
  joinMatch(matchId: string): Promise<void>;

  sendMatchData(matchId: string, op: number, data: object): void;

  onMatchData(cb: (msg: any) => void): void;
  getLeaderboardTop(): Promise<ApiLeaderboardRecordList>;
  listOpenMatches(): Promise<ApiMatch[]>;
}

export const NakamaContext = createContext<NakamaContextType>(null!);

export function NakamaProvider({ children }: { children: React.ReactNode }) {
  console.log(
    "[Nakama] Initializing...",
      import.meta.env.VITE_WS_SKEY,
      import.meta.env.VITE_WS_HOST,
      import.meta.env.VITE_WS_PORT,
      import.meta.env.VITE_WS_SSL === "true"
  );
  const [client] = useState(
    () => new Client(
      import.meta.env.VITE_WS_SKEY,
      import.meta.env.VITE_WS_HOST,
      import.meta.env.VITE_WS_PORT,
      import.meta.env.VITE_WS_SSL === "true"
    )
  );

  const [session, setSession] = useState<Session | null>(null);
  const [socket, setSocket] = useState<Socket | null>(null);
  const [matchId, setMatchId] = useState<string | null>(null);
  const lastModeRef = React.useRef<string | null>(null);
  const socketRef = React.useRef<Socket | null>(null);

  async function autoLogin() {
    const deviceId = getOrCreateDeviceId();

    try {
      return await client.authenticateDevice(
        deviceId,
        false
      );
    } catch (e) {
      // fallback: treat as new user
      localStorage.removeItem("registered");
      throw e;
    }
  }

  async function registerWithUsername(username: string) {
    const deviceId = getOrCreateDeviceId();

    // create + set username
    const session = await client.authenticateDevice(
      deviceId,
      true,
      username
    );

    // mark an account as registered
    localStorage.setItem("registered", "yes");
    localStorage.setItem("username", username);

    return session;
  }

  async function getSession(username?: string) {
    const isRegistered = localStorage.getItem("registered") === "yes";
    if (!username && !isRegistered) {
      throw new Error("No username provided and not registered");
    }

    let newSession;
    if (!isRegistered) {
      newSession = await registerWithUsername(username ?? "");
    } else {
      newSession = await autoLogin();
    }

    return newSession;
  }

  // ----------------------------------------------------
  // LOGIN
  // ----------------------------------------------------
  async function loginOrRegister(username?: string) {
    // authenticate user
    const newSession = await getSession(username);
    setSession(newSession);

    // create a socket (new Nakama 3.x signature)
    const s = client.createSocket(undefined, undefined); // no SSL on localhost
    await s.connect(newSession, true);
    setSocket(s);
    socketRef.current = s;

    console.log("[Nakama] WebSocket connected");

    // MATCHMAKER MATCHED CALLBACK
    s.onmatchmakermatched = async (matched: MatchmakerMatched) => {
      // 1) If match_id is empty → server rejected the group.
      if (!matched.match_id) {
        console.warn("[Nakama] Match rejected by server. Auto-requeueing...");

        if (lastModeRef.current) {
          try {
            await joinMatchmaker(lastModeRef.current);
          } catch (e) {
            console.error("[Nakama] Requeue failed:", e);
          }
        }

        return;
      }

      // 2) Valid match: continue as usual.
      console.log("[Nakama] MATCHED:", matched);
      try {
        await s.joinMatch(matched.match_id);
        setMatchId(matched.match_id);
        console.log("[Nakama] Auto-joined match:", matched.match_id);
      } catch (err) {
        console.error("[Nakama] Failed to join match:", err);
      }
    };
  }

  async function logout() {
    try {
      // 1) Disconnect socket if present
      if (socketRef.current) {
        socketRef.current.disconnect(true);
        console.log("[Nakama] WebSocket disconnected");
      }
    } catch (err) {
      console.warn("[Nakama] Error while disconnecting socket:", err);
    }

    // 2) Clear state
    setSocket(null);
    socketRef.current = null;
    setSession(null);

    console.log("[Nakama] Clean logout completed");
  }

  // ----------------------------------------------------
  // MATCHMAKING
  // ----------------------------------------------------
  async function joinMatchmaker(mode: string) {
    const socket = socketRef.current;
    if (!socket) throw new Error("socket missing");

    console.log(`[Nakama] Matchmaking... with +mode:"${mode}"`);
    const ticket: MatchmakerTicket = await socket.addMatchmaker(
      `*`, // query
      2, // min count
      2, // max count
      { mode } // stringProperties
    );

    lastModeRef.current = mode;

    return ticket.ticket;
  }

  // ----------------------------------------------------
  // EXPLICIT MATCH JOIN
  // ----------------------------------------------------
  async function joinMatch(id: string) {
    if (!socket) throw new Error("socket missing");
    await socket.joinMatch(id);
    setMatchId(id);
    console.log("[Nakama] Joined match", id);
  }

  // ----------------------------------------------------
  // MATCH STATE SEND
  // ----------------------------------------------------
  function sendMatchData(matchId: string, op: number, data: object) {
    if (!socket) return;
    socket.sendMatchState(matchId, op, JSON.stringify(data));
  }

  // ----------------------------------------------------
  // MATCH DATA LISTENER
  // ----------------------------------------------------
  function onMatchData(cb: (msg: any) => void) {
    if (!socket) return;

    socket.onmatchdata = (m: MatchData) => {
      const decoded = JSON.parse(new TextDecoder().decode(m.data));
      cb({
        opCode: m.op_code,
        data: decoded,
        userId: m.presence?.user_id ?? null,
      });
    };
  }

  async function getLeaderboardTop(): Promise<ApiLeaderboardRecordList> {
    if (!session) return [];

    return await client.listLeaderboardRecords(
      session,
      "tictactoe",
      [],
      10 // top 10
    );
  }
  async function listOpenMatches(): Promise<ApiMatch[]> {
    if (!session) {
      console.warn("[Nakama] listOpenMatches called before login");
      return [];
    }

    const result = await client.listMatches(session, 10);

    console.log("[Nakama] Open matches:", result.matches);

    return result.matches ?? [];
  }

  return (
    <NakamaContext.Provider
      value={{
        client,
        session,
        socket,
        matchId,
        loginOrRegister,
        logout,
        joinMatchmaker,
        joinMatch,
        sendMatchData,
        onMatchData,
        getLeaderboardTop,
        listOpenMatches,
      }}
    >
      {children}
    </NakamaContext.Provider>
  );
}

// ---------------------------------------------
// USE HOOK
// ---------------------------------------------
export function useNakama(): NakamaContextType {
  const ctx = useContext(NakamaContext);
  if (!ctx) throw new Error("useNakama must be inside a NakamaProvider");
  return ctx;
}
