import { CurrencyMeta, InitResp, LineCoords, Money, Paytable } from '../../types/api';
import { BalancePolicy } from '../modelInit/BalancePolicy';
import { BetService } from '../modelInit/BetService';
import { FeatureMultiplierCalculator } from '../modelInit/FeatureMultiplierCalculator';
import { GameDataFactory } from '../modelInit/GameDataFactory';
import { ModesFactory } from '../modelInit/ModesFactory';
import { OutcomeRestorer } from '../modelInit/OutcomeRestorer';

import type OutcomesView from '../helpers/OutcomesView';

export interface BootstrapDeps
{
    outcomeRestorer: OutcomeRestorer;
    multiplierCalc: FeatureMultiplierCalculator;
    gameDataFactory: GameDataFactory;
    modesFactory: ModesFactory;
    balancePolicy: BalancePolicy;
    OutcomesViewCtor: new (raw: any) => any;
}

export class ModelBootstrap
{
    constructor(private d: BootstrapDeps) {}

    init(rawData: InitResp, machineConfig: any)
    {
        const restored: InitResp = this.d.outcomeRestorer.restore(rawData);
        if (!restored?.options?.base_bet)
        {
            throw new Error('base_bet is not defined');
        }

        const outcomes: OutcomesView = new this.d.OutcomesViewCtor(restored);
        const currency: CurrencyMeta = restored.options.currency;
        const paytableData: Paytable = restored.options.paytable;
        const baseBet: Money = restored.options.base_bet;

        const featureMultipliers = this.d.multiplierCalc.calc(restored);

        const betService = new BetService(restored, outcomes);
        const gameData = this.d.gameDataFactory.create(restored);
        const lines = this.resolveLines(restored.options.lines, machineConfig);

        // main
        const mainBets = betService.getMainBets();

        mainBets.current = betService.getLastUserBetIndex();
        const main = this.d.modesFactory.createMain(mainBets, gameData, lines);

        const modes: any[] = [main];

        // casino rounds
        if (gameData.isCasinoRoundsTriggered || gameData.isCasinoRoundsActive)
        {
            const crBets = betService.getCasinoRoundsBets();

            modes.push(this.d.modesFactory.createCasinoRounds(crBets, gameData, lines));
        }

        // bonus
        if (
            gameData.isRespinsGameRestored || gameData.isFreespinsGameRestored
      || gameData.isFreespinsGameFinished || gameData.isRespinsGameFinished
        )
        {
            const bonusBets = betService.getBonusBets(gameData);

            modes.push(this.d.modesFactory.createBonus(bonusBets, gameData, lines));
        }

        this.d.balancePolicy.applyPreventAddBalanceFlag(modes);

        return {
            balance: restored.balance.wallet,
            currency,
            modes,
            featureMultipliers,
            paytableData,
            baseBet,
            machineConfig
        };
    }

    /**
     * Resolves the line configuration from init payload or falls back to the provided machine config.
     * Returns an empty collection when no configuration is supplied, enabling cluster-style games.
     * @param optionsLines Raw lines array received from backend options.
     * @param machineConfig Slot machine configuration that may contain a lines definition.
     */
    private resolveLines(optionsLines: unknown, machineConfig: unknown): LineCoords[]
    {
        const normalized = this.normalizeLines(optionsLines);

        if (normalized)
        {
            return normalized;
        }

        const fallbackCandidate = this.extractLinesFromConfig(machineConfig);
        const fallback = this.normalizeLines(fallbackCandidate);

        if (fallback)
        {
            return fallback;
        }

        const hasRawOptionsLines = optionsLines !== null && optionsLines !== undefined;
        const hasRawFallbackLines = fallbackCandidate !== null && fallbackCandidate !== undefined;

        if (hasRawOptionsLines || hasRawFallbackLines)
        {
            throw new Error('Invalid win lines configuration provided.');
        }

        return [];
    }

    /**
     * Converts loosely typed lines data into a normalized array of numeric coordinates.
     * Accepts arrays of numbers or numeric strings and clones them to keep inputs immutable.
     * @param source Candidate lines payload.
     */
    private normalizeLines(source: unknown): LineCoords[] | null
    {
        if (!Array.isArray(source))
        {
            return null;
        }

        const normalized: LineCoords[] = [];

        for (let lineIndex = 0; lineIndex < source.length; lineIndex += 1)
        {
            const rawLine = source[lineIndex];

            if (!Array.isArray(rawLine))
            {
                return null;
            }

            const normalizedLine: number[] = [];

            for (let columnIndex = 0; columnIndex < rawLine.length; columnIndex += 1)
            {
                const rawPosition = rawLine[columnIndex];
                const numeric = typeof rawPosition === 'number' ? rawPosition : Number(rawPosition);

                if (!Number.isFinite(numeric))
                {
                    return null;
                }

                normalizedLine.push(Math.trunc(numeric));
            }

            normalized.push(normalizedLine);
        }

        return normalized;
    }

    /**
     * Attempts to discover a lines definition within an arbitrary machine configuration object.
     * Supports multiple property names and walks nested objects defensively.
     * @param machineConfig Potential source of a lines configuration.
     */
    private extractLinesFromConfig(machineConfig: unknown): unknown
    {
        const visited = new Set<unknown>();
        const candidateKeys = ['lines', 'lineCoords', 'line_coordinates', 'paylines', 'pay_lines'];

        const traverse = (value: unknown): unknown =>
        {
            if (!value || typeof value !== 'object' || visited.has(value))
            {
                return null;
            }

            visited.add(value);

            const record = value as Record<string, unknown>;

            for (const key of candidateKeys)
            {
                if (key in record)
                {
                    return record[key];
                }
            }

            for (const nested of Object.values(record))
            {
                const found = traverse(nested);

                if (found)
                {
                    return found;
                }
            }

            return null;
        };

        return traverse(machineConfig);
    }
}
