import Locator from '../../services/Locator';
import { getLatestSpinKey, getLatestSpinTimeKey } from '../../utils/utils';
import AutoGameModel, { AutoGameConfig } from '../AutoGameModel';
import BaseGameData from '../BaseGameData';
import BetModel from '../BetModel';
import Lines from '../Lines';

import type { BackendErrorDetail } from '../../errors/BackendError';
import type { ApiError, InitResp, SpinResp } from '../../types/api';
import type BalanceModel from '../BalanceModel';
import type { LinesConfig } from '../Lines';

/** Minimal bet configuration consumed by {@link Mode}. */
export interface ModeBetConfig
{
    bets: number[];
    current: number;
    defaultBet: number;
}

/** Constructor signature for a compatible game data implementation. */
export type GameDataRawPayload = InitResp | SpinResp;

export type GameDataConstructor = new (rawData: GameDataRawPayload) => BaseGameData;

type LinesSource = Lines | LinesConfig | null | undefined;

/** Base class for all game modes. Manages bets, wins, auto game flow and persistence helpers. */
export default class Mode
{
    public static gameDataClass: GameDataConstructor = BaseGameData;

    public static createGameData(rawData: GameDataRawPayload): BaseGameData
    {
        return new this.gameDataClass(rawData);
    }

    public static provideGameDataClass(gameDataClass: GameDataConstructor): void
    {
        this.gameDataClass = gameDataClass;
    }

    protected _name: string;

    protected _totalWin: number;

    protected _win: number;

    protected _gameData: BaseGameData;

    private readonly _lines: Lines;

    public readonly betModel: BetModel;

    protected _betFeatureActive: boolean;

    private _isBuyBonusBlocked: boolean;

    public autoGame: AutoGameModel | null;

    protected _isRoundStarted: boolean;

    private _isAutoGameLocked: boolean;

    protected constructor(
        name: string,
        { bets, current, defaultBet }: ModeBetConfig,
        gameData: BaseGameData,
        lines: LinesSource,
    )
    {
        this._name = name;
        this._totalWin = 0;
        this._win = 0;
        this._gameData = gameData;
        this._lines = this._createLinesModel(lines);
        this.betModel = this._createBetModel({ bets, current, defaultBet });
        this._betFeatureActive = false;
        this._isBuyBonusBlocked = false;
        this.autoGame = null;
        this._isRoundStarted = false;
        this._isAutoGameLocked = false;

        this.lockBets();
        this.lockBonusBuy();
        this.lockAutoGame();
    }

    public get name(): string
    {
        return this._name;
    }

    public get scene(): string
    {
        throw new Error('The scene\'s name should be rewritten in extended class');
    }

    public get lines(): LinesConfig
    {
        return this._lines.lines;
    }

    public get bet(): number
    {
        return this.betModel.bet;
    }

    public get bets(): readonly number[]
    {
        return this.betModel.bets;
    }

    public get totalWin(): number
    {
        return this._totalWin;
    }

    public set totalWin(totalWin: number)
    {
        this._totalWin = totalWin;
    }

    /** Win amount that should be returned to the previous mode. */
    public get translatedTotalWin(): number
    {
        return this._totalWin;
    }

    public get win(): number
    {
        return this._win;
    }

    public set win(win: number)
    {
        this._win = win;
    }

    public get gameData(): BaseGameData
    {
        return this._gameData;
    }

    /** Compact data for bonus indicator widgets. */
    public get bonusIndicatorData(): { title: string; issued: number; left: number }
    {
        return {
            title: 'main',
            issued: 0,
            left: 0,
        };
    }

    public isBonusMode(): boolean
    {
        return false;
    }

    /** Compact data for casino round indicators. */
    public get casinoRoundsIndicatorData(): { issued: number; left: number }
    {
        return {
            issued: 0,
            left: 0,
        };
    }

    public isCasinoRoundsMode(): boolean
    {
        return false;
    }

    /**
     * Starts a round and prepares auto/bet states.
     * @param _balanceModel
     * @param _multiplier
     */
    public async startRound(_balanceModel: BalanceModel, _multiplier = 1): Promise<void>
    {
        if (this._isRoundStarted)
        {
            return;
        }

        this._isRoundStarted = true;
        this.resetWin();
        this.lockBets();
        this.lockBonusBuy();
        this.lockAutoGame();
        this._gameData = null as unknown as BaseGameData;
        this.autoGame?.startSpin();
    }

    /**
     * Marks the end of a round.
     * @param _balanceModel
     * @param _multiplier
     */
    public endRound(_balanceModel: BalanceModel, _multiplier = 1): void
    {
        this._isRoundStarted = false;
    }

    /**
     * Completes post-round housekeeping.
     * @param balanceModel
     */
    public afterRoundEnding(balanceModel: BalanceModel): void
    {
        this.addWinToBalance(balanceModel);
        this.checkAutoGame(balanceModel);
        this.unlockBets();
        this.unlockBonusBuy();
        this.unlockAutoGame();
    }

    /** Indicates whether a round is active. */
    public isRoundStarted(): boolean
    {
        return this._isRoundStarted;
    }

    /**
     * Handles spin button press for base behaviour.
     * @param balanceModel
     * @param multiplier
     */
    public async performSpinButtonAction(balanceModel: BalanceModel, multiplier = 1): Promise<void>
    {
        if (!this.isRoundStarted())
        {
            await this.startRound(balanceModel, multiplier);
        }
    }

    public canBuyBonus(): boolean
    {
        return !this._isBuyBonusBlocked && !this.autoGame;
    }

    public lockBonusBuy(): void
    {
        this._isBuyBonusBlocked = true;
    }

    public unlockBonusBuy(): void
    {
        this._isBuyBonusBlocked = false;
    }

    public buyFreespins(_balanceModel: BalanceModel, _multiplier = 1): void {}

    public buyBonus(_balanceModel: BalanceModel, _multiplier = 1): void {}

    public get betFeatureActive(): boolean
    {
        return this._betFeatureActive;
    }

    public toggleBetFeatureActive(): void {}

    /** Increases the current bet if possible. */
    public increaseBet(): void
    {
        this.betModel.increaseBet();
    }

    /** Decreases the current bet if possible. */
    public decreaseBet(): void
    {
        this.betModel.decreaseBet();
    }

    /** Sets the bet to the maximum value. */
    public setToMaxBet(): void
    {
        this.betModel.setToMaxBet();
    }

    /**
     * Sets the bet by its value if allowed.
     * @param value
     */
    public setBetByValue(value: number): void
    {
        this.betModel.setBetByValue(value);
    }

    public canIncreaseBet(): boolean
    {
        return this.betModel.canIncreaseBet();
    }

    public canDecreaseBet(): boolean
    {
        return this.betModel.canDecreaseBet();
    }

    public lockBets(): void
    {
        this.betModel.lock();
    }

    public unlockBets(): void
    {
        this.betModel.unlock();
    }

    public areBetsLocked(): boolean
    {
        return this.betModel.isLocked();
    }

    /** Checks if auto game can be started in this mode. */
    public canStartAutoGame(): boolean
    {
        return !this.isRoundStarted() && !this._isAutoGameLocked && !this.autoGame;
    }

    /** Indicates if the mode supports auto spin by default. */
    public shouldAutoSpin(): boolean
    {
        return false;
    }

    /**
     * Starts an auto game with the specified configuration.
     * @param balanceModel
     * @param config
     * @param multiplier
     */
    public async startAutoGame(balanceModel: BalanceModel, config: AutoGameConfig, multiplier: number): Promise<void>
    {
        if (!this.canStartAutoGame())
        {
            throw new Error('Auto game cannot be started in this mode.');
        }

        this.autoGame = this._createAutoGameModel(config);
        await this.startRound(balanceModel, multiplier);
    }

    /** Stops the current auto game. */
    public stopAutoGame(): void
    {
        if (!this.isAutoGameActive())
        {
            throw new Error('No active auto game to stop.');
        }

        this.autoGame = null;
    }

    public isAutoGameActive(): boolean
    {
        return this.autoGame?.isActive() || false;
    }

    public getAutoGameIssued(): number
    {
        return this.autoGame?.spinCount.issued || 0;
    }

    public getAutoGameLeft(): number
    {
        if (!this.autoGame)
        {
            return 0;
        }

        const { issued, current } = this.autoGame.spinCount;

        return issued - current;
    }

    public lockAutoGame(): void
    {
        this._isAutoGameLocked = true;
    }

    public addWinToBalance(balanceModel: BalanceModel): void
    {
        if (this.gameData.preventAddBalance)
        {
            return;
        }

        balanceModel.add(Math.max(this._win, this._totalWin));
    }

    public checkAutoGame(balanceModel: BalanceModel): void
    {
        const bonusGameWon = this.gameData.bonusSpins.issued > 0;

        this.autoGame?.endSpin(balanceModel.getBalance(), this._win, bonusGameWon);

        if (this.autoGame && !this.autoGame.isActive())
        {
            this.autoGame = null;
        }
    }

    public unlockAutoGame(): void
    {
        this._isAutoGameLocked = false;
    }

    protected _createAutoGameModel(config: AutoGameConfig): AutoGameModel
    {
        return new AutoGameModel(config);
    }

    public resetWin(): void
    {
        this.win = 0;
    }

    public resetTotalWin(): void
    {
        this.totalWin = 0;
    }

    public addWinAmount(value: number): void
    {
        this.win += value;
    }

    /**
     * Normalizes provided line data into a {@link Lines} instance with an empty fallback.
     * @param lines Raw lines configuration supplied by bootstrap or tests.
     */
    private _createLinesModel(lines: LinesSource): Lines
    {
        if (lines instanceof Lines)
        {
            return lines;
        }

        return new Lines(lines ?? []);
    }

    public isExitConditionMet(): boolean
    {
        return false;
    }

    private _createBetModel({ bets, current, defaultBet }: ModeBetConfig): BetModel
    {
        if (!Array.isArray(bets) || bets.length === 0)
        {
            throw new Error('Invalid bets array: must be a non-empty array of numbers.');
        }

        if (typeof current !== 'number' || current < 0 || current >= bets.length)
        {
            throw new Error(`Invalid current bet index: must be between 0 and ${bets.length - 1}.`);
        }

        if (typeof defaultBet !== 'number' || defaultBet < 0 || defaultBet >= bets.length)
        {
            throw new Error(`Invalid default bet index: must be between 0 and ${bets.length - 1}.`);
        }

        return new BetModel(bets, current, defaultBet);
    }

    public saveResponseToStorage(response: unknown): void
    {
        Locator.storage.set(getLatestSpinKey(), response);
        Locator.storage.set(getLatestSpinTimeKey(), Date.now());
    }

    public removeResponseFromStorage(): void
    {
        Locator.storage.delete(getLatestSpinKey());
        Locator.storage.delete(getLatestSpinTimeKey());
    }

    /**
     * Maps raw API errors into {@link BackendErrorDetail} objects.
     * @param errors
     */
    protected mapErrors(errors?: ApiError[]): BackendErrorDetail[]
    {
        return (errors ?? []).map(({ code, desc, ...rest }) => ({
            code,
            message: desc,
            ...rest,
        }));
    }

    // Hooks for subclasses -------------------------------------------------

    public onEnter(): void {}

    public onExit(): void {}

    public beforeExit(_balanceModel: BalanceModel): void {}
}
