import { Assets } from 'pixi.js';
import { transpose } from '../utils/utils';

import type {
    Flow,
    FlowCommand,
    FlowState,
    FreeRounds,
    InitResp,
    LineWin,
    Money,
    Outcome,
    ScreenMap,
    SpinResp,
    SymbolId,
    Win,
} from '../types/api';

/** Union of raw API envelopes consumed by {@link BaseGameData}. */
type GameResponse = InitResp | SpinResp;

interface SlotMachineAsset
{
    symbolsKeys: readonly string[] | Record<string, string>;
}

/** Flattened screen cell entry keyed by machine symbol. */
export interface ScreenSymbol
{
    key: string;
    value: number;
}

/** Compact casino rounds counters exposed to modes/UI. */
export interface CasinoRoundsState
{
    issued: number;
    left: number;
    status: FreeRounds['status'];
}

/** Generic bonus (free/respins) counters persisted in the game data. */
export interface BonusSpinsCounters
{
    issued: number;
    left: number;
}

const DEFAULT_BONUS_SPINS: BonusSpinsCounters = { issued: 0, left: 0 };

/** Maps raw API payloads into cached game data values. */
export default class BaseGameData
{
    public rawData: GameResponse;

    private _isInit = false;

    private _preventAddBalance = false;

    public balance: Money;

    public gameTotalWin: Money;

    public winInRound: Money;

    public freeRoundsTotalWin: Money;

    public screenMap: ScreenSymbol[];

    public wins: LineWin[] | null;

    public lastActionID: number;

    public state: FlowState;

    public command: FlowCommand;

    public casinoRounds: CasinoRoundsState | undefined;

    public bonusSpins: BonusSpinsCounters;

    public wilds: unknown;

    public constructor(rawData: GameResponse)
    {
        this.rawData = rawData;
        this.balance = rawData.balance.wallet;
        this.gameTotalWin = rawData.balance.game;
        this.winInRound = 0;
        this.freeRoundsTotalWin = rawData.balance.free_rounds ?? 0;
        this.screenMap = [];
        this.wins = [];
        this.lastActionID = BaseGameData.parseLastActionIndex(rawData.flow);
        this.state = rawData.flow.state;
        this.command = rawData.flow.command;
        this.casinoRounds = undefined;
        this.bonusSpins = { ...DEFAULT_BONUS_SPINS };
        this.wilds = undefined;

        if (BaseGameData.shouldInitFromOptions(rawData))
        {
            this._initFromOptions(rawData);
        }
        else
        {
            this._initFromOutcome(rawData);
        }

        this._initCasinoRoundsData(rawData);
        this._initBonusSpinsData(rawData);
    }

    public get isInit(): boolean
    {
        return this._isInit;
    }

    public set isInit(isInit: boolean)
    {
        this._isInit = isInit;
    }

    public get preventAddBalance(): boolean
    {
        return this._preventAddBalance;
    }

    public set preventAddBalance(preventAddBalance: boolean)
    {
        this._preventAddBalance = preventAddBalance;
    }

    /** Resets transient win data when a round returns to idle. */
    public resetDataToIdleState(): void
    {
        this.wins = null;
    }

    /** Signals if the backend reported a win limit lock. */
    public get isWinLimitReached(): boolean | undefined
    {
        const storage = this.rawData.outcome?.storage as Record<string, unknown> | undefined;

        return storage?.win_limit_reached as boolean | undefined;
    }

    /**
     * Return true if a respins game or a freespins game was triggered
     * @returns {boolean}
     */
    public get isBonusGameTriggered(): boolean
    {
        return this.isRespinsGameTriggered || this.isFreespinsGameTriggered;
    }

    public get isRespinsGameTriggered(): boolean
    {
        return (this.lastActionID === 1) && this.state === 'respin';
    }

    public get isRespinsGameRestored(): boolean
    {
        return (this.lastActionID > 1) && this.command === 'init' && this.state === 'respin';
    }

    public get isRespinsGameFinished(): boolean
    {
        return this.state === 'closed' && this.command === 'respin';
    }

    public get isFreespinsGameTriggered(): boolean
    {
        return (this.lastActionID === 1) && this.state === 'freespins';
    }

    public get isFreespinsGameRestored(): boolean
    {
        return (this.lastActionID > 1) && this.command === 'init' && this.state === 'freespins';
    }

    public get isFreespinsGameFinished(): boolean
    {
        return this.state === 'closed' && this.command === 'freespin';
    }

    public get isCasinoRoundsTriggered(): boolean
    {
        const rounds = this.casinoRounds;

        if (!rounds)
        {
            return false;
        }

        return rounds.left === rounds.issued && this.isCasinoRoundsActive;
    }

    public get isCasinoRoundsRestored(): boolean
    {
        const rounds = this.casinoRounds;

        if (!rounds || !this.isCasinoRoundsActive)
        {
            return false;
        }

        if (this.state === 'freespins')
        {
            return false;
        }

        if (this.state === 'respins')
        {
            return false;
        }

        return this.isInit && rounds.left > 0 && rounds.left < rounds.issued;
    }

    public get isCasinoRoundsFinished(): boolean
    {
        const rounds = this.casinoRounds;

        if (!rounds)
        {
            return false;
        }

        return ['closed', 'ready'].includes(this.state) && rounds.left === 0;
    }

    public get isCasinoRoundsAvailable(): boolean
    {
        return this.casinoRounds?.status === 'available';
    }

    public get isCasinoRoundsActive(): boolean
    {
        const status = this.casinoRounds?.status;

        return status === 'active' || status === 'played';
    }

    /**
     * Reports whether the current flow belongs to a freespins sequence.
     * Covers active/restore states in addition to direct `freespin` commands.
     */
    public get isFreeSpinsState(): boolean
    {
        return this._isBonusFlow('freespin', 'freespins');
    }

    /**
     * Reports whether the current flow belongs to a respins sequence.
     * Covers active/restore states in addition to direct `respin` commands.
     */
    public get isRespinsState(): boolean
    {
        return this._isBonusFlow('respin', ['respin', 'respins']);
    }

    /**
     * Normalizes flow checks for freespins/respins regardless of the reported command.
     * @param expectedCommand Bonus command that should mark the bonus sequence.
     * @param expectedState Flow state(s) corresponding to the bonus sequence.
     */
    private _isBonusFlow(expectedCommand: FlowCommand, expectedState: FlowState | readonly FlowState[]): boolean
    {
        const states = Array.isArray(expectedState) ? expectedState : [expectedState];

        if (states.includes(this.state))
        {
            return true;
        }

        return this.command === expectedCommand || (this.state === 'closed' && this.command === expectedCommand);
    }

    private _initCasinoRoundsData(rawData: GameResponse): void
    {
        const freeRounds = rawData.free_rounds;

        if (!freeRounds)
        {
            return;
        }

        const issued = Number(freeRounds.count) || 0;
        const performed = Number(freeRounds.performed) || 0;

        this.casinoRounds = {
            issued,
            left: Math.max(issued - performed, 0),
            status: freeRounds.status,
        };
    }

    private _initBonusSpinsData(rawData: GameResponse): void
    {
        this.bonusSpins = { ...DEFAULT_BONUS_SPINS };

        const features = rawData.features;

        if (!features)
        {
            return;
        }

        if (this.isRespinsState)
        {
            this.bonusSpins.issued = features.respins_issued ?? 0;
            this.bonusSpins.left = features.respins_left ?? 0;
        }
        else if (this.isFreeSpinsState)
        {
            this.bonusSpins.issued = features.freespins_issued ?? 0;
            this.bonusSpins.left = features.freespins_left ?? 0;
        }
    }

    private _initFromOptions(rawData: InitResp): void
    {
        const optionsWithStorage = rawData.options as InitResp['options'] & { storage?: Record<string, unknown> };

        this.wilds = optionsWithStorage.storage?.wilds;
        this.screenMap = this._setupScreenMap(transpose(optionsWithStorage.screen));
        this.wins = [];
        this.winInRound = 0;
    }

    private _initFromOutcome(rawData: GameResponse): void
    {
        const outcome = (rawData as SpinResp).outcome as Outcome;
        const storage = outcome.storage as Record<string, unknown> | undefined;

        this.wilds = storage?.wilds;
        this.screenMap = this._setupScreenMap(transpose(outcome.screen));
        this.wins = this._setupWinsMap(outcome.wins);
        this.winInRound = outcome.win;
    }

    /**
     * Flattens the API screen grid into symbol entries while preserving coordinates for overrides.
     * @param screen - Screen map fetched from the backend.
     */
    private _setupScreenMap(screen: ScreenMap): ScreenSymbol[]
    {
        return screen
            .map((row, y) => row.map((rawSymbolName, x) => this._getSymbolsKeyValue(rawSymbolName, x, y)))
            .flat();
    }

    /**
     * Directly maps raw symbol identifiers via slot machine config, mirroring JS runtime semantics.
     * @param rawSymbolName - Identifier returned by the backend.
     * @param x - Column index within the screen grid.
     * @param y - Row index within the screen grid.
     */
    private _getSymbolsKeyValue(rawSymbolName: SymbolId, x?: number, y?: number): ScreenSymbol
    {
        const slotMachineConfig = Assets.get('slotMachine') as SlotMachineAsset;
        const { symbolsKeys } = slotMachineConfig;

        void x;
        void y;

        return { key: (symbolsKeys as Record<string, string | undefined>)[rawSymbolName] as string, value: 0 };
    }

    private _setupWinsMap(wins: Win[] | undefined): LineWin[]
    {
        return (wins as Win[])
            .filter(BaseGameData.isLineWin)
            .sort((prev, next) => next[1] - prev[1]);
    }

    private static parseLastActionIndex(flow: Flow): number
    {
        const lastActionId = flow.last_action_id;

        if (typeof lastActionId !== 'string')
        {
            return 0;
        }

        const [, actionIndex] = lastActionId.split('_');
        const parsed = Number(actionIndex);

        return Number.isFinite(parsed) ? parsed : 0;
    }

    private static hasOptions(rawData: GameResponse): rawData is InitResp
    {
        return 'options' in rawData && Boolean(rawData.options);
    }

    /** Mirrors legacy JS logic for choosing between options or outcome payloads. */
    private static shouldInitFromOptions(rawData: GameResponse): rawData is InitResp
    {
        const spinPayload = rawData as SpinResp;
        const hasOutcome = Boolean(spinPayload.outcome);
        const outcomesHistory = spinPayload.outcomes;
        const hasOutcomeHistory = Array.isArray(outcomesHistory) && outcomesHistory.length > 0;

        return !hasOutcome && !hasOutcomeHistory && BaseGameData.hasOptions(rawData);
    }

    private static isLineWin(win: Win): win is LineWin
    {
        return win[0] === 'line';
    }
}
