import { Mutex } from "../deps.ts";
import { RankState, State } from "./state.ts";
import { Splatnet3 } from "./splatnet3.ts";
import {
  BattleListNode,
  ChallengeProgress,
  CoopHistoryGroups,
  CoopInfo,
  Game,
  HistoryGroupItem,
  VsInfo,
  VsMode,
} from "./types.ts";
import { Cache, MemoryCache } from "./cache.ts";
import { gameId } from "./utils.ts";
import { RankTracker } from "./RankTracker.ts";

/**
 * Fetch game and cache it. It also fetches bankara match challenge info.
 * if splatnet is not given, it will use cache only
 */
export class GameFetcher {
  private _splatnet?: Splatnet3;
  private cache: Cache;
  private rankTracker: RankTracker;

  private lock: Record<string, Mutex | undefined> = {};
  private bankaraLock = new Mutex();
  private bankaraHistory?: HistoryGroupItem<BattleListNode>[];
  private coopLock = new Mutex();
  private coopHistory?: CoopHistoryGroups["nodes"];
  private xMatchLock = new Mutex();
  private xMatchHistory?: HistoryGroupItem<BattleListNode>[];

  constructor(
    { cache = new MemoryCache(), splatnet, state }: {
      splatnet?: Splatnet3;
      state: State;
      cache?: Cache;
    },
  ) {
    this._splatnet = splatnet;
    this.cache = cache;
    this.rankTracker = new RankTracker(state.rankState);
  }
  private get splatnet() {
    if (!this._splatnet) {
      throw new Error("splatnet is not set");
    }
    return this._splatnet;
  }
  private getLock(id: string): Mutex {
    let cur = this.lock[id];

    if (!cur) {
      cur = new Mutex();
      this.lock[id] = cur;
    }

    return cur;
  }

  setRankState(state: RankState | undefined) {
    this.rankTracker.setState(state);
  }

  async updateRank(): Promise<RankState | undefined> {
    const finalState = await this.rankTracker.updateState(
      await this.getBankaraHistory(),
    );
    return finalState;
  }

  getRankStateById(id: string) {
    return this.rankTracker.getRankStateById(id);
  }

  getXMatchHistory() {
    if (!this._splatnet) {
      return [];
    }
    return this.xMatchLock.use(async () => {
      if (this.xMatchHistory) {
        return this.xMatchHistory;
      }

      const { xBattleHistories: { historyGroups } } = await this.splatnet
        .getXBattleHistories();

      this.xMatchHistory = historyGroups.nodes;

      return this.xMatchHistory;
    });
  }
  getBankaraHistory() {
    if (!this._splatnet) {
      return [];
    }
    return this.bankaraLock.use(async () => {
      if (this.bankaraHistory) {
        return this.bankaraHistory;
      }

      const { bankaraBattleHistories: { historyGroups } } = await this.splatnet
        .getBankaraBattleHistories();

      this.bankaraHistory = historyGroups.nodes;

      return this.bankaraHistory;
    });
  }
  getCoopHistory() {
    if (!this._splatnet) {
      return [];
    }
    return this.coopLock.use(async () => {
      if (this.coopHistory) {
        return this.coopHistory;
      }

      const { coopResult: { historyGroups } } = await this.splatnet
        .getCoopHistories();

      this.coopHistory = historyGroups.nodes;

      return this.coopHistory;
    });
  }
  async getCoopMetaById(id: string): Promise<Omit<CoopInfo, "detail">> {
    const coopHistory = this._splatnet ? await this.getCoopHistory() : [];
    const group = coopHistory.find((i) =>
      i.historyDetails.nodes.some((i) => i.id === id)
    );

    if (!group) {
      return {
        type: "CoopInfo",
        listNode: null,
        groupInfo: null,
        gradeBefore: null,
      };
    }

    const { historyDetails, ...groupInfo } = group;
    const listNodeIdx = historyDetails.nodes.findIndex((i) => i.id === id) ??
      null;
    const listNode = listNodeIdx !== null
      ? historyDetails.nodes[listNodeIdx]
      : null;
    const listNodeBefore = listNodeIdx !== null
      ? (historyDetails.nodes[listNodeIdx + 1] ?? null)
      : null;

    return {
      type: "CoopInfo",
      listNode,
      groupInfo,
      gradeBefore: listNodeBefore?.afterGrade && listNodeBefore.afterGradePoint
        ? {
          grade: listNodeBefore.afterGrade,
          gradePoint: listNodeBefore.afterGradePoint,
        }
        : null,
    };
  }
  async getBattleMetaById(
    id: string,
    vsMode: VsMode,
  ): Promise<Omit<VsInfo, "detail">> {
    const gid = await gameId(id);

    const gameIdMap = new Map<BattleListNode, string>();
    let group: HistoryGroupItem<BattleListNode> | null = null;
    let listNode: BattleListNode | null = null;

    if (vsMode === "BANKARA" || vsMode === "X_MATCH") {
      const bankaraHistory = vsMode === "BANKARA"
        ? await this.getBankaraHistory()
        : await this.getXMatchHistory();

      for (const i of bankaraHistory) {
        for (const j of i.historyDetails.nodes) {
          gameIdMap.set(j, await gameId(j.id));
        }
      }

      group = bankaraHistory.find((i) =>
        i.historyDetails.nodes.some((i) =>
          gameIdMap.get(i) === gid
        )
      ) ?? null;
    }

    if (!group) {
      return {
        type: "VsInfo",
        challengeProgress: null,
        bankaraMatchChallenge: null,
        listNode: null,
        rankState: null,
        rankBeforeState: null,
        groupInfo: null,
      };
    }

    const { bankaraMatchChallenge, xMatchMeasurement } = group;
    const { historyDetails, ...groupInfo } = group;
    listNode = historyDetails.nodes.find((i) => gameIdMap.get(i) === gid) ??
      null;
    const index = historyDetails.nodes.indexOf(listNode!);

    let challengeProgress: null | ChallengeProgress = null;
    const challengeOrMeasurement = bankaraMatchChallenge || xMatchMeasurement;
    if (challengeOrMeasurement) {
      const pastBattles = historyDetails.nodes.slice(0, index);
      const { winCount, loseCount } = challengeOrMeasurement;
      challengeProgress = {
        index,
        winCount: winCount -
          pastBattles.filter((i) => i.judgement == "WIN").length,
        loseCount: loseCount -
          pastBattles.filter((i) =>
            ["LOSE", "DEEMED_LOSE"].includes(i.judgement)
          ).length,
      };
    }

    const { before, after } = await this.rankTracker.getRankStateById(id) ??
      {};

    return {
      type: "VsInfo",
      bankaraMatchChallenge,
      listNode,
      challengeProgress,
      rankState: after ?? null,
      rankBeforeState: before ?? null,
      groupInfo,
    };
  }
  private cacheDetail<T>(
    id: string,
    getter: () => Promise<T>,
  ): Promise<T> {
    const lock = this.getLock(id);

    return lock.use(async () => {
      const cached = await this.cache.read<T>(id);
      if (cached) {
        return cached;
      }

      const detail = await getter();

      await this.cache.write(id, detail);

      return detail;
    });
  }
  fetch(type: Game["type"], id: string): Promise<Game> {
    switch (type) {
      case "VsInfo":
        return this.fetchBattle(id);
      case "CoopInfo":
        return this.fetchCoop(id);
      default:
        throw new Error(`Unknown game type: ${type}`);
    }
  }
  private async fetchBattle(id: string): Promise<VsInfo> {
    const detail = await this.cacheDetail(
      id,
      () => this.splatnet.getBattleDetail(id).then((r) => r.vsHistoryDetail),
    );
    const metadata = await this.getBattleMetaById(id, detail.vsMode.mode);

    const game: VsInfo = {
      ...metadata,
      detail,
    };

    return game;
  }
  private async fetchCoop(id: string): Promise<CoopInfo> {
    const detail = await this.cacheDetail(
      id,
      () => this.splatnet.getCoopDetail(id).then((r) => r.coopHistoryDetail),
    );
    const metadata = await this.getCoopMetaById(id);

    const game: CoopInfo = {
      ...metadata,
      detail,
    };

    return game;
  }
}
