import type { Achievement, BossChallengeResponse, GameState } from "@elysium/types";
import {
  createContext,
  useCallback,
  useContext,
  useEffect,
  useRef,
  useState,
} from "react";
import { challengeBoss as challengeBossApi, loadGame, saveGame } from "../api/client.js";
import { applyTick, calculateClickPower } from "../engine/tick.js";

export interface BattleResult {
  bossName: string;
  result: BossChallengeResponse;
}

interface GameContextValue {
  state: GameState | null;
  isLoading: boolean;
  error: string | null;
  /** Click the crystal to earn gold */
  handleClick: () => void;
  /** Buy an adventurer */
  buyAdventurer: (adventurerId: string) => void;
  /** Buy an upgrade */
  buyUpgrade: (upgradeId: string) => void;
  /** Purchase a buyable equipment item */
  buyEquipment: (equipmentId: string) => void;
  /** Start a quest */
  startQuest: (questId: string) => void;
  /** Challenge a boss — runs full server-side simulation */
  challengeBoss: (bossId: string) => Promise<void>;
  /** Equip an owned equipment item (auto-unequips the same slot) */
  equipItem: (equipmentId: string) => void;
  /** Reload state from the server */
  reload: () => Promise<void>;
  /** Offline gold earned on login */
  offlineGold: number;
  /** Dismiss the offline gold notification */
  dismissOfflineGold: () => void;
  /** Battle result to display in the modal (null when no battle pending) */
  battleResult: BattleResult | null;
  /** Dismiss the battle result modal */
  dismissBattle: () => void;
  /** Queue of newly unlocked achievements (for toasts) */
  newAchievements: Achievement[];
  /** Remove an achievement from the toast queue */
  dismissAchievement: (id: string) => void;
}

const GameContext = createContext<GameContextValue | null>(null);

const AUTO_SAVE_INTERVAL_MS = 30_000;

export const GameProvider = ({ children }: { children: React.ReactNode }): React.JSX.Element => {
  const [state, setState] = useState<GameState | null>(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  const [offlineGold, setOfflineGold] = useState(0);
  const [battleResult, setBattleResult] = useState<BattleResult | null>(null);
  const [newAchievements, setNewAchievements] = useState<Achievement[]>([]);
  const stateRef = useRef<GameState | null>(null);
  const lastSaveRef = useRef<number>(Date.now());
  const rafRef = useRef<number | null>(null);
  const newlyUnlockedRef = useRef<Achievement[]>([]);

  stateRef.current = state;

  const reload = useCallback(async () => {
    setIsLoading(true);
    setError(null);
    try {
      const data = await loadGame();
      setState(data.state);
      if (data.offlineGold > 0) {
        setOfflineGold(data.offlineGold);
      }
    } catch (err) {
      setError(err instanceof Error ? err.message : "Failed to load game");
    } finally {
      setIsLoading(false);
    }
  }, []);

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

  // Game loop via requestAnimationFrame
  useEffect(() => {
    if (!state) return;

    let lastTime = performance.now();

    const tick = (now: number): void => {
      const deltaSeconds = (now - lastTime) / 1000;
      lastTime = now;

      setState((prev) => {
        if (!prev) return prev;
        const next = applyTick(prev, deltaSeconds);

        // Detect newly unlocked achievements
        newlyUnlockedRef.current = next.achievements.filter((a, i) => {
          const wasLocked = (prev.achievements ?? [])[i]?.unlockedAt === null;
          return wasLocked && a.unlockedAt !== null;
        });

        return next;
      });

      if (newlyUnlockedRef.current.length > 0) {
        setNewAchievements((prev) => [...prev, ...newlyUnlockedRef.current]);
        newlyUnlockedRef.current = [];
      }

      // Auto-save every 30 seconds
      if (Date.now() - lastSaveRef.current >= AUTO_SAVE_INTERVAL_MS) {
        lastSaveRef.current = Date.now();
        if (stateRef.current) {
          void saveGame({ state: stateRef.current });
        }
      }

      rafRef.current = requestAnimationFrame(tick);
    };

    rafRef.current = requestAnimationFrame(tick);
    return () => {
      if (rafRef.current !== null) {
        cancelAnimationFrame(rafRef.current);
      }
    };
  // eslint-disable-next-line react-hooks/exhaustive-deps -- only run when state becomes available
  }, [state !== null]);

  const handleClick = useCallback(() => {
    setState((prev) => {
      if (!prev) return prev;
      const clickPower = calculateClickPower(prev);
      return {
        ...prev,
        resources: { ...prev.resources, gold: prev.resources.gold + clickPower },
        player: {
          ...prev.player,
          totalGoldEarned: prev.player.totalGoldEarned + clickPower,
          totalClicks: prev.player.totalClicks + 1,
        },
      };
    });
  }, []);

  const buyAdventurer = useCallback((adventurerId: string) => {
    setState((prev) => {
      if (!prev) return prev;
      const adventurer = prev.adventurers.find((a) => a.id === adventurerId);
      if (!adventurer || !adventurer.unlocked) return prev;

      const cost = 10 * Math.pow(1.15, adventurer.count);
      if (prev.resources.gold < cost) return prev;

      return {
        ...prev,
        resources: { ...prev.resources, gold: prev.resources.gold - cost },
        adventurers: prev.adventurers.map((a) =>
          a.id === adventurerId ? { ...a, count: a.count + 1 } : a,
        ),
      };
    });
  }, []);

  const buyUpgrade = useCallback((upgradeId: string) => {
    setState((prev) => {
      if (!prev) return prev;
      const upgrade = prev.upgrades.find((u) => u.id === upgradeId);
      if (!upgrade || !upgrade.unlocked || upgrade.purchased) return prev;
      if (prev.resources.gold < upgrade.costGold) return prev;
      if (prev.resources.essence < upgrade.costEssence) return prev;
      if (prev.resources.crystals < (upgrade.costCrystals ?? 0)) return prev;

      return {
        ...prev,
        resources: {
          ...prev.resources,
          gold: prev.resources.gold - upgrade.costGold,
          essence: prev.resources.essence - upgrade.costEssence,
          crystals: prev.resources.crystals - (upgrade.costCrystals ?? 0),
        },
        upgrades: prev.upgrades.map((u) =>
          u.id === upgradeId ? { ...u, purchased: true } : u,
        ),
      };
    });
  }, []);

  const startQuest = useCallback((questId: string) => {
    setState((prev) => {
      if (!prev) return prev;
      const quest = prev.quests.find((q) => q.id === questId);
      if (!quest || quest.status !== "available") return prev;

      return {
        ...prev,
        quests: prev.quests.map((q) =>
          q.id === questId
            ? { ...q, status: "active" as const, startedAt: Date.now() }
            : q,
        ),
      };
    });
  }, []);

  const equipItem = useCallback((equipmentId: string) => {
    setState((prev) => {
      if (!prev) return prev;
      const item = (prev.equipment ?? []).find((e) => e.id === equipmentId);
      if (!item || !item.owned) return prev;

      return {
        ...prev,
        equipment: (prev.equipment ?? []).map((e) => {
          if (e.id === equipmentId) return { ...e, equipped: true };
          // Unequip the previously-equipped item in the same slot
          if (e.type === item.type && e.equipped) return { ...e, equipped: false };
          return e;
        }),
      };
    });
  }, []);

  const buyEquipment = useCallback((equipmentId: string) => {
    setState((prev) => {
      if (!prev) return prev;
      const item = (prev.equipment ?? []).find((e) => e.id === equipmentId);
      if (!item || item.owned || !item.cost) return prev;

      const { gold, essence, crystals } = item.cost;
      if (prev.resources.gold < gold) return prev;
      if (prev.resources.essence < essence) return prev;
      if (prev.resources.crystals < crystals) return prev;

      const slotAlreadyEquipped = (prev.equipment ?? []).some(
        (e) => e.type === item.type && e.equipped,
      );

      return {
        ...prev,
        resources: {
          ...prev.resources,
          gold: prev.resources.gold - gold,
          essence: prev.resources.essence - essence,
          crystals: prev.resources.crystals - crystals,
        },
        equipment: (prev.equipment ?? []).map((e) => {
          if (e.id === equipmentId) return { ...e, owned: true, equipped: !slotAlreadyEquipped };
          return e;
        }),
      };
    });
  }, []);

  const challengeBoss = useCallback(async (bossId: string) => {
    if (!stateRef.current) return;
    const boss = stateRef.current.bosses.find((b) => b.id === bossId);
    if (!boss) return;

    try {
      const result = await challengeBossApi({ bossId });

      // Update local state to match server result
      setState((prev) => {
        if (!prev) return prev;

        if (result.won) {
          const defeatedBoss = prev.bosses.find((b) => b.id === bossId);
          const zoneBosses = prev.bosses.filter((b) => b.zoneId === defeatedBoss?.zoneId);
          const zoneIdx = zoneBosses.findIndex((b) => b.id === bossId);
          const nextZoneBossId = zoneBosses[zoneIdx + 1]?.id;

          // Find newly unlocked zones and their first bosses
          // A zone unlocks when BOTH the gate boss is defeated AND the gate quest is completed
          const newlyUnlockedZones = (prev.zones ?? []).filter((z) => {
            if (z.status !== "locked" || z.unlockBossId !== bossId) return false;
            const questOk =
              z.unlockQuestId == null ||
              prev.quests.some((q) => q.id === z.unlockQuestId && q.status === "completed");
            return questOk;
          });
          const newZoneFirstBossIds = newlyUnlockedZones.map((z) => {
            const firstBoss = prev.bosses.find((b) => b.zoneId === z.id);
            return firstBoss?.id;
          }).filter(Boolean);

          return {
            ...prev,
            bosses: prev.bosses.map((b) => {
              if (b.id === bossId) return { ...b, status: "defeated" as const, currentHp: 0 };
              if (b.id === nextZoneBossId && b.prestigeRequirement <= prev.prestige.count) {
                return { ...b, status: "available" as const };
              }
              if (newZoneFirstBossIds.includes(b.id) && b.prestigeRequirement <= prev.prestige.count) {
                return { ...b, status: "available" as const };
              }
              return b;
            }),
            zones: (prev.zones ?? []).map((z) => {
              if (z.status !== "locked" || z.unlockBossId !== bossId) return z;
              const questOk =
                z.unlockQuestId == null ||
                prev.quests.some((q) => q.id === z.unlockQuestId && q.status === "completed");
              return questOk ? { ...z, status: "unlocked" as const } : z;
            }),
            resources: result.rewards
              ? {
                  ...prev.resources,
                  gold: prev.resources.gold + result.rewards.gold,
                  essence: prev.resources.essence + result.rewards.essence,
                  crystals: prev.resources.crystals + result.rewards.crystals,
                }
              : prev.resources,
            player: result.rewards
              ? {
                  ...prev.player,
                  totalGoldEarned:
                    prev.player.totalGoldEarned + result.rewards.gold,
                }
              : prev.player,
            upgrades: result.rewards
              ? prev.upgrades.map((u) =>
                  result.rewards!.upgradeIds.includes(u.id)
                    ? { ...u, unlocked: true }
                    : u,
                )
              : prev.upgrades,
            equipment: result.rewards
              ? (prev.equipment ?? []).map((e) => {
                  if (!result.rewards!.equipmentIds.includes(e.id)) return e;
                  const slotEmpty = !(prev.equipment ?? []).some(
                    (other) => other.type === e.type && other.equipped,
                  );
                  return { ...e, owned: true, equipped: slotEmpty || e.equipped };
                })
              : prev.equipment ?? [],
          };
        }

        // Loss: reset boss HP and apply casualties
        return {
          ...prev,
          bosses: prev.bosses.map((b) =>
            b.id === bossId
              ? { ...b, status: "available" as const, currentHp: b.maxHp }
              : b,
          ),
          adventurers: prev.adventurers.map((a) => {
            const casualty = result.casualties?.find(
              (c) => c.adventurerId === a.id,
            );
            if (!casualty) return a;
            return { ...a, count: Math.max(0, a.count - casualty.killed) };
          }),
        };
      });

      setBattleResult({ bossName: boss.name, result });
    } catch {
      // Silently ignore — server errors shouldn't crash the UI
    }
  }, []);

  const dismissOfflineGold = useCallback(() => {
    setOfflineGold(0);
  }, []);

  const dismissBattle = useCallback(() => {
    setBattleResult(null);
  }, []);

  const dismissAchievement = useCallback((id: string) => {
    setNewAchievements((prev) => prev.filter((a) => a.id !== id));
  }, []);

  return (
    <GameContext.Provider
      value={{
        state,
        isLoading,
        error,
        handleClick,
        buyAdventurer,
        buyUpgrade,
        buyEquipment,
        startQuest,
        challengeBoss,
        equipItem,
        reload,
        offlineGold,
        dismissOfflineGold,
        battleResult,
        dismissBattle,
        newAchievements,
        dismissAchievement,
      }}
    >
      {children}
    </GameContext.Provider>
  );
};

export const useGame = (): GameContextValue => {
  const context = useContext(GameContext);
  if (!context) {
    throw new Error("useGame must be used within a GameProvider");
  }
  return context;
};
