import type { AppScreen } from "../frontend/types";
import type { FrontendSnapshot } from "../frontend/store";
import { useFrontendStore } from "../frontend/store";
import { emitControllerToken, setExternalControllerMovement, type ControllerMovement, type ControllerTokenEvent } from "../input/controller";
import { type GameState, useGameStore } from "../game/store";
import type { AbilityId, BottomTab, MemberId, RunBuffId } from "../game/types";
import type { BossId, HealerClassId } from "../game/types";
import type { GameModeId, GameSettings, SaveSlotId } from "../frontend/types";
import type { GearOwnerId, GearSlotId } from "../game/progression/gear";
import type { DifficultySlug } from "../game/progression/loot";

const CHANNEL_NAME = "i-want-to-heal:thor-dual-screen:v1";

export type GameCommand =
  | { name: "startEncounter" }
  | { name: "restart" }
  | { name: "castAbility"; abilityId: AbilityId }
  | { name: "selectMember"; memberId: MemberId }
  | { name: "cycleMember"; direction: 1 | -1 }
  | { name: "setActiveTab"; tab: BottomTab }
  | { name: "selectItem"; itemId: string }
  | { name: "setPaused"; paused: boolean }
  | { name: "setPauseSelection"; selection: "resume" | "exit" }
  | { name: "setSelectedRunBuff"; buffId: RunBuffId }
  | { name: "chooseRunBuff"; buffId: RunBuffId }
  | { name: "continueRoguelikeRound" }
  | { name: "startRogueTrialsEndless" }
  | { name: "setEndlessChoiceSelection"; selection: "continue" | "quit" };

export type FrontendCommand =
  | { name: "signIn"; username: string; password: string }
  | { name: "createAccount"; username: string; password: string }
  | { name: "continueOffline" }
  | { name: "signOut" }
  | { name: "navigate"; screen: AppScreen }
  | { name: "selectSlot"; slotId: SaveSlotId }
  | { name: "createSlot"; slotId: SaveSlotId; hunterName: string }
  | { name: "playSlot"; slotId: SaveSlotId }
  | { name: "deleteSlot"; slotId: SaveSlotId }
  | { name: "copySlot"; sourceId: SaveSlotId; targetId: SaveSlotId }
  | { name: "uploadSlot"; slotId: SaveSlotId }
  | { name: "downloadSlot"; slotId: SaveSlotId }
  | { name: "selectMode"; mode: GameModeId }
  | { name: "selectBoss"; bossId: BossId }
  | { name: "selectDifficulty"; difficultySlug: DifficultySlug }
  | { name: "selectGearOwner"; ownerId: GearOwnerId }
  | { name: "selectGearSlot"; slotId: GearSlotId }
  | { name: "selectGearWorkshopMode"; mode: "upgrade" | "infusion" }
  | { name: "selectInfusion"; infusionId: string }
  | { name: "selectPassiveAbility"; abilityId: AbilityId }
  | { name: "selectPassiveInfusion"; passiveId: RunBuffId }
  | { name: "upgradeSelectedGear" }
  | { name: "equipSelectedInfusion" }
  | { name: "equipPassiveInfusion"; passiveId: RunBuffId }
  | { name: "selectHealerClass"; classId: HealerClassId }
  | { name: "updateSetting"; key: keyof GameSettings; value: GameSettings[keyof GameSettings] }
  | { name: "exitGame" }
  | { name: "launchGame"; bossIds: readonly BossId[]; difficultySlug?: DifficultySlug };

export const DUAL_SCREEN_LAUNCH_EVENT = "iwt:dual-screen-launch-game";
export const DUAL_SCREEN_EXIT_EVENT = "iwt:dual-screen-exit-game";

export type DualScreenMessage =
  | { type: "app-state"; screen: AppScreen; hunterName: string | null; notice: string; frontend?: FrontendSnapshot; game?: Partial<BottomGameSnapshot> }
  | { type: "controller-token"; id: string; event: ControllerTokenEvent }
  | { type: "controller-echo"; id: string; event: ControllerTokenEvent }
  | { type: "controller-motion"; movement: ControllerMovement }
  | { type: "game-command"; command: GameCommand }
  | { type: "frontend-command"; command: FrontendCommand }
  | { type: "authoritative-ready" }
  | { type: "companion-ready" }
  | { type: "companion-closing" };

export function createDualScreenChannel() {
  return typeof BroadcastChannel === "undefined" ? null : new BroadcastChannel(CHANNEL_NAME);
}

export function executeGameCommand(command: GameCommand) {
  const game = useGameStore.getState();
  switch (command.name) {
    case "startEncounter": game.startEncounter(); break;
    case "restart": game.restart(); break;
    case "castAbility": game.castAbility(command.abilityId); break;
    case "selectMember": game.selectMember(command.memberId); break;
    case "cycleMember": game.cycleMember(command.direction); break;
    case "setActiveTab": game.setActiveTab(command.tab); break;
    case "selectItem": game.selectItem(command.itemId); break;
    case "setPaused": game.setPaused(command.paused); break;
    case "setPauseSelection": game.setPauseSelection(command.selection); break;
    case "setSelectedRunBuff": game.setSelectedRunBuff(command.buffId); break;
    case "chooseRunBuff": game.chooseRunBuff(command.buffId); break;
    case "continueRoguelikeRound": game.continueRoguelikeRound(); break;
    case "startRogueTrialsEndless": game.startRogueTrialsEndless(); break;
    case "setEndlessChoiceSelection": game.setEndlessChoiceSelection(command.selection); break;
  }
}

export function executeFrontendCommand(command: FrontendCommand) {
  const frontend = useFrontendStore.getState();
  switch (command.name) {
    case "signIn": void frontend.signIn(command.username, command.password); break;
    case "createAccount": void frontend.createAccount(command.username, command.password); break;
    case "continueOffline": frontend.continueOffline(); break;
    case "signOut": frontend.signOut(); break;
    case "navigate": frontend.navigate(command.screen); break;
    case "selectSlot": frontend.selectSlot(command.slotId); break;
    case "createSlot": frontend.createSlot(command.slotId, command.hunterName); break;
    case "playSlot": frontend.playSlot(command.slotId); break;
    case "deleteSlot": frontend.deleteSlot(command.slotId); break;
    case "copySlot": frontend.copySlot(command.sourceId, command.targetId); break;
    case "uploadSlot": frontend.uploadSlot(command.slotId); break;
    case "downloadSlot": frontend.downloadSlot(command.slotId); break;
    case "selectMode": frontend.selectMode(command.mode); break;
    case "selectBoss": frontend.selectBoss(command.bossId); break;
    case "selectDifficulty": frontend.selectDifficulty(command.difficultySlug); break;
    case "selectGearOwner": frontend.selectGearOwner(command.ownerId); break;
    case "selectGearSlot": frontend.selectGearSlot(command.slotId); break;
    case "selectGearWorkshopMode": frontend.selectGearWorkshopMode(command.mode); break;
    case "selectInfusion": frontend.selectInfusion(command.infusionId); break;
    case "selectPassiveAbility": frontend.selectPassiveAbility(command.abilityId); break;
    case "selectPassiveInfusion": frontend.selectPassiveInfusion(command.passiveId); break;
    case "upgradeSelectedGear": frontend.upgradeSelectedGear(); break;
    case "equipSelectedInfusion": frontend.equipSelectedInfusion(); break;
    case "equipPassiveInfusion": frontend.equipPassiveInfusion(command.passiveId); break;
    case "selectHealerClass": frontend.selectHealerClass(command.classId); break;
    case "updateSetting": frontend.updateSetting(command.key, command.value); break;
    case "exitGame": window.dispatchEvent(new Event(DUAL_SCREEN_EXIT_EVENT)); break;
    case "launchGame": window.dispatchEvent(new CustomEvent(DUAL_SCREEN_LAUNCH_EVENT, { detail: { bossIds: command.bossIds, difficultySlug: command.difficultySlug } })); break;
  }
}

export function receiveAuthoritativeMessage(message: DualScreenMessage) {
  if (message.type === "controller-token") emitControllerToken(message.event);
  if (message.type === "controller-motion") setExternalControllerMovement(message.movement);
  if (message.type === "game-command") executeGameCommand(message.command);
  if (message.type === "frontend-command") executeFrontendCommand(message.command);
}

/** State the lower display actually renders. Renderer-only and progression data stay local to the authoritative screen. */
export type BottomGameSnapshot = Pick<GameState,
  | "bossId"
  | "bossInstanceId"
  | "paused"
  | "healerClassId"
  | "phase"
  | "round"
  | "endlessMode"
  | "endlessBossKills"
  | "endlessChoiceSelection"
  | "runModifiers"
  | "time"
  | "party"
  | "boss"
  | "additionalBosses"
  | "partyPositions"
  | "bossMotion"
  | "partyCombat"
  | "mana"
  | "maxMana"
  | "selectedMemberId"
  | "cooldowns"
  | "globalCooldownUntil"
  | "activeTab"
  | "selectedItemId"
  | "inventory"
  | "playerPosition"
  | "activeCast"
  | "barrier"
>;

const BOTTOM_GAME_SNAPSHOT_KEYS: readonly (keyof BottomGameSnapshot)[] = [
  "bossId", "bossInstanceId", "paused", "healerClassId", "phase", "round", "endlessMode", "endlessBossKills", "endlessChoiceSelection", "runModifiers", "time", "party", "boss", "additionalBosses",
  "partyPositions", "bossMotion", "partyCombat", "mana", "maxMana", "selectedMemberId", "cooldowns",
  "globalCooldownUntil", "activeTab", "selectedItemId", "inventory", "playerPosition", "activeCast", "barrier",
];

function structurallyEqual(left: unknown, right: unknown): boolean {
  if (Object.is(left, right)) return true;
  if (!left || !right || typeof left !== "object" || typeof right !== "object") return false;
  if (Array.isArray(left) || Array.isArray(right)) {
    if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) return false;
    for (let index = 0; index < left.length; index += 1) {
      if (!structurallyEqual(left[index], right[index])) return false;
    }
    return true;
  }
  const leftRecord = left as Record<string, unknown>;
  const rightRecord = right as Record<string, unknown>;
  const keys = Object.keys(leftRecord);
  if (keys.length !== Object.keys(rightRecord).length) return false;
  return keys.every((key) => Object.prototype.hasOwnProperty.call(rightRecord, key) && structurallyEqual(leftRecord[key], rightRecord[key]));
}

export function currentBottomGameSnapshot(): BottomGameSnapshot {
  const state = useGameStore.getState();
  return {
    bossId: state.bossId,
    bossInstanceId: state.bossInstanceId,
    paused: state.paused,
    healerClassId: state.healerClassId,
    phase: state.phase,
    round: state.round,
    endlessMode: state.endlessMode,
    endlessBossKills: state.endlessBossKills,
    endlessChoiceSelection: state.endlessChoiceSelection,
    runModifiers: state.runModifiers,
    time: state.time,
    party: state.party,
    boss: state.boss,
    additionalBosses: state.additionalBosses,
    partyPositions: state.partyPositions,
    bossMotion: state.bossMotion,
    partyCombat: state.partyCombat,
    mana: state.mana,
    maxMana: state.maxMana,
    selectedMemberId: state.selectedMemberId,
    cooldowns: state.cooldowns,
    globalCooldownUntil: state.globalCooldownUntil,
    activeTab: state.activeTab,
    selectedItemId: state.selectedItemId,
    inventory: state.inventory,
    playerPosition: state.playerPosition,
    activeCast: state.activeCast,
    barrier: state.barrier,
  };
}

export function diffBottomGameSnapshot(
  previous: BottomGameSnapshot | undefined,
  next: BottomGameSnapshot,
): Partial<BottomGameSnapshot> {
  if (!previous) return next;
  const changes: Partial<BottomGameSnapshot> = {};
  for (const key of BOTTOM_GAME_SNAPSHOT_KEYS) {
    if (!structurallyEqual(previous[key], next[key])) Object.assign(changes, { [key]: next[key] });
  }
  return changes;
}
