import type { Achievement, BossChallengeResponse, GameState, NumberFormat } from "@elysium/types";
import {
  createContext,
  useCallback,
  useContext,
  useEffect,
  useRef,
  useState,
} from "react";
import {
  buyPrestigeUpgrade as buyPrestigeUpgradeApi,
  challengeBoss as challengeBossApi,
  loadGame,
  prestige as prestigeApi,
  saveGame,
} from "../api/client.js";
import { RESOURCE_CAP, applyTick, calculateClickPower } from "../engine/tick.js";
import { updateChallengeProgress } from "../utils/dailyChallenges.js";
import { formatNumber as formatNumberUtil } from "../utils/format.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>;
  /** Unix timestamp of the last successful cloud save (null until first save response) */
  lastSavedAt: number | null;
  /** True whilst a forced save is in-flight */
  isSyncing: boolean;
  /** Immediately save to the server and reset the auto-save timer */
  forceSync: () => Promise<void>;
  /** Error message from the last failed cloud save (null when no error) */
  syncError: string | null;
  /** Offline gold earned on login */
  offlineGold: number;
  /** Offline essence earned on login */
  offlineEssence: number;
  /** Dismiss the offline earnings 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;
  /** The player's chosen number display format */
  numberFormat: NumberFormat;
  /** Update the number format preference (persisted to server via profile save) */
  setNumberFormat: (format: NumberFormat) => void;
  /** Format a number using the player's chosen notation style */
  formatNumber: (value: number) => string;
  /** Buy a prestige upgrade from the runestone shop */
  buyPrestigeUpgrade: (upgradeId: string) => Promise<void>;
  /** Toggle the auto-prestige setting on/off (requires auto_prestige upgrade) */
  toggleAutoPrestige: () => void;
}

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

const AUTO_SAVE_INTERVAL_MS = 30_000;
const AUTO_PRESTIGE_THRESHOLD_BASE = 1_000_000;
const AUTO_PRESTIGE_THRESHOLD_SCALE = 5;

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 [offlineEssence, setOfflineEssence] = useState(0);
  const [battleResult, setBattleResult] = useState<BattleResult | null>(null);
  const [newAchievements, setNewAchievements] = useState<Achievement[]>([]);
  const [lastSavedAt, setLastSavedAt] = useState<number | null>(null);
  const [isSyncing, setIsSyncing] = useState(false);
  const [syncError, setSyncError] = useState<string | null>(null);
  const syncErrorTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  const [numberFormat, setNumberFormat] = useState<NumberFormat>("suffix");
  const stateRef = useRef<GameState | null>(null);
  const lastSaveRef = useRef<number>(Date.now());
  const isSyncingRef = useRef(false);
  const rafRef = useRef<number | null>(null);
  const newlyUnlockedRef = useRef<Achievement[]>([]);
  const signatureRef = useRef<string | null>(localStorage.getItem("elysium_save_signature"));
  const isAutoPrestigingRef = useRef(false);
  const reloadRef = useRef<() => Promise<void>>(() => Promise.resolve());

  stateRef.current = state;

  const reload = useCallback(async () => {
    setIsLoading(true);
    setError(null);
    try {
      const data = await loadGame();
      setState(data.state);
      setLastSavedAt(data.state.player.lastSavedAt);
      if (data.signature) {
        signatureRef.current = data.signature;
        localStorage.setItem("elysium_save_signature", data.signature);
      }
      if (data.offlineGold > 0) {
        setOfflineGold(data.offlineGold);
      }
      if (data.offlineEssence > 0) {
        setOfflineEssence(data.offlineEssence);
      }
      // Fetch number format preference from profile (fire-and-forget, non-blocking)
      void fetch(`/api/profile/${data.state.player.discordId}`)
        .then(async (res) => {
          if (!res.ok) return;
          const profile = await res.json() as { profileSettings?: { numberFormat?: NumberFormat } };
          const fmt = profile.profileSettings?.numberFormat;
          if (fmt === "suffix" || fmt === "scientific" || fmt === "engineering") {
            setNumberFormat(fmt);
          }
        })
        .catch(() => { /* fall back to default "suffix" */ });
    } catch (err) {
      setError(err instanceof Error ? err.message : "Failed to load game");
    } finally {
      setIsLoading(false);
    }
  }, []);

  reloadRef.current = reload;

  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 (skip if a force sync is in-flight to avoid signature collisions)
      if (Date.now() - lastSaveRef.current >= AUTO_SAVE_INTERVAL_MS) {
        lastSaveRef.current = Date.now();
        if (stateRef.current && !isSyncingRef.current) {
          void saveGame({
            state: stateRef.current,
            ...(signatureRef.current !== null ? { signature: signatureRef.current } : {}),
          }).then((response) => {
            setLastSavedAt(response.savedAt);
            if (response.signature) {
              signatureRef.current = response.signature;
              localStorage.setItem("elysium_save_signature", response.signature);
            }
          }).catch((err: unknown) => {
            // Silently clear a bad signature so the next auto-save can proceed
            if (err instanceof Error && err.message.includes("signature mismatch")) {
              signatureRef.current = null;
              localStorage.removeItem("elysium_save_signature");
            }
          });
        }
      }

      // Auto-prestige: fire when unlocked, enabled, and threshold is met
      const autoState = stateRef.current;
      if (
        !isAutoPrestigingRef.current &&
        autoState?.prestige.purchasedUpgradeIds.includes("auto_prestige") &&
        autoState.prestige.autoPrestigeEnabled &&
        autoState.player.totalGoldEarned >=
          AUTO_PRESTIGE_THRESHOLD_BASE *
            Math.pow(AUTO_PRESTIGE_THRESHOLD_SCALE, autoState.prestige.count)
      ) {
        isAutoPrestigingRef.current = true;
        void prestigeApi({ characterName: autoState.player.characterName })
          .then(() => reloadRef.current())
          .catch(() => { /* silently ignore — will retry next tick */ })
          .finally(() => { isAutoPrestigingRef.current = false; });
      }

      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 showSyncError = useCallback((message: string) => {
    setSyncError(message);
    if (syncErrorTimerRef.current) clearTimeout(syncErrorTimerRef.current);
    syncErrorTimerRef.current = setTimeout(() => { setSyncError(null); }, 5000);
  }, []);

  const clearBadSignature = useCallback(() => {
    signatureRef.current = null;
    localStorage.removeItem("elysium_save_signature");
  }, []);

  const forceSync = useCallback(async () => {
    if (!stateRef.current || isSyncingRef.current) return;
    isSyncingRef.current = true;
    lastSaveRef.current = Date.now(); // push auto-save timer back so it doesn't fire concurrently
    setIsSyncing(true);
    try {
      const response = await saveGame({
        state: stateRef.current,
        ...(signatureRef.current !== null ? { signature: signatureRef.current } : {}),
      });
      setSyncError(null);
      setLastSavedAt(response.savedAt);
      lastSaveRef.current = Date.now();
      if (response.signature) {
        signatureRef.current = response.signature;
        localStorage.setItem("elysium_save_signature", response.signature);
      }
    } catch (err) {
      const message = err instanceof Error ? err.message : "Save failed";
      showSyncError(message);
      if (message.includes("signature mismatch")) clearBadSignature();
    } finally {
      isSyncingRef.current = false;
      setIsSyncing(false);
    }
  }, [showSyncError, clearBadSignature]);

  const handleClick = useCallback(() => {
    setState((prev) => {
      if (!prev) return prev;
      const clickPower = calculateClickPower(prev);
      const newGold = Math.min(prev.resources.gold + clickPower, RESOURCE_CAP);

      let updatedDailyChallenges = prev.dailyChallenges;
      let challengeCrystals = 0;
      if (updatedDailyChallenges) {
        const result = updateChallengeProgress(updatedDailyChallenges, "clicks", 1);
        updatedDailyChallenges = result.updatedChallenges;
        challengeCrystals = result.crystalsAwarded;
      }

      return {
        ...prev,
        resources: {
          ...prev.resources,
          gold: newGold,
          crystals: Math.min(prev.resources.crystals + challengeCrystals, RESOURCE_CAP),
        },
        player: {
          ...prev.player,
          totalGoldEarned: prev.player.totalGoldEarned + clickPower,
          totalClicks: prev.player.totalClicks + 1,
        },
        ...(updatedDailyChallenges !== undefined ? { dailyChallenges: updatedDailyChallenges } : {}),
      };
    });
  }, []);

  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 buyPrestigeUpgrade = useCallback(async (upgradeId: string) => {
    try {
      const result = await buyPrestigeUpgradeApi({ upgradeId });
      setState((prev) => {
        if (!prev) return prev;
        return {
          ...prev,
          prestige: {
            ...prev.prestige,
            runestones: result.runestonesRemaining,
            purchasedUpgradeIds: result.purchasedUpgradeIds,
            runestonesIncomeMultiplier: result.runestonesIncomeMultiplier,
            runestonesClickMultiplier: result.runestonesClickMultiplier,
            runestonesEssenceMultiplier: result.runestonesEssenceMultiplier,
            runestonesCrystalMultiplier: result.runestonesCrystalMultiplier,
          },
        };
      });
    } catch {
      // Silently ignore — server errors shouldn't crash the UI
    }
  }, []);

  const toggleAutoPrestige = useCallback(() => {
    setState((prev) => {
      if (!prev) return prev;
      return {
        ...prev,
        prestige: {
          ...prev.prestige,
          autoPrestigeEnabled: !prev.prestige.autoPrestigeEnabled,
        },
      };
    });
  }, []);

  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,
            prestige: result.rewards?.bountyRunestones
              ? {
                  ...prev.prestige,
                  runestones: prev.prestige.runestones + result.rewards.bountyRunestones,
                }
              : prev.prestige,
            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);
    setOfflineEssence(0);
  }, []);

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

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

  const boundFormatNumber = useCallback(
    (value: number) => formatNumberUtil(value, numberFormat),
    [numberFormat],
  );

  return (
    <GameContext.Provider
      value={{
        state,
        isLoading,
        error,
        handleClick,
        buyAdventurer,
        buyUpgrade,
        buyEquipment,
        startQuest,
        challengeBoss,
        equipItem,
        reload,
        lastSavedAt,
        isSyncing,
        forceSync,
        syncError,
        offlineGold,
        offlineEssence,
        dismissOfflineGold,
        battleResult,
        dismissBattle,
        newAchievements,
        dismissAchievement,
        numberFormat,
        setNumberFormat,
        formatNumber: boundFormatNumber,
        buyPrestigeUpgrade,
        toggleAutoPrestige,
      }}
    >
      {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;
};
