import Locator from '../../services/Locator';
import { loadLastSetUserBet } from '../../utils/utils';

import type { Balance, BaseFeatures, Flow, InitResp, Outcome, SpinResp } from '../../types/api';

export interface StorageLike
{
    get<T = unknown>(key: string): T | null;
}

export interface OutcomeRestorerDeps
{
    storage: StorageLike;
    maxAgeMs: number;
    getSpinKey(): string;
    getSpinTimeKey(): string;
    now(): number;
}

type SavedSpin = Pick<
SpinResp,
'api_version' | 'balance' | 'features' | 'flow' | 'outcome' | 'outcomes' | 'free_rounds'
>;

/**
 * Restores the last valid spin outcome when the init response comes without results.
 * Skips restoration if casino-provided free rounds are present.
 */
export class OutcomeRestorer
{
    constructor(private readonly d: OutcomeRestorerDeps) {}

    /**
     * Checks if restoration should be skipped because casino free rounds are active/available.
     * If `free_rounds` is present in InitResp, the backend is in a special mode
     * and we should not reuse previous spin results.
     * @param raw
     */
    shouldSkipBecauseOfCasinoRounds(raw: InitResp): boolean
    {
        return Boolean(raw.free_rounds);
    }

    /**
     * Attempts to restore missing outcome data in an `init` response from a saved previous spin.
     * Will not restore if:
     * - Casino free rounds are present
     * - No saved spin in storage
     * - Init already contains an outcome/outcomes
     * - The user has changed the bet since last spin
     * @param raw
     */
    restore(raw: InitResp): InitResp
    {
        if (!Locator.storage.checkAvailable())
        {
            return this.cloneBaseline(raw);
        }

        if (this.shouldSkipBecauseOfCasinoRounds(raw))
        {
            return this.cloneBaseline(raw);
        }

        const saved = this.getSavedSpin();

        if (!saved) return this.cloneBaseline(raw);

        // Do not restore if init already has a non-empty outcome or outcomes list
        if (this.hasAnyOutcome(raw)) return this.cloneBaseline(raw);

        // Do not restore if the bet value was changed by the user
        if (loadLastSetUserBet().isChanged) return this.cloneBaseline(raw);

        // Start from a shallow copy of init and merge in relevant saved fields
        const base = this.cloneBaseline(raw);
        const clone: InitResp = { ...base };

        // If init.outcomes is empty but saved.outcomes exists, restore them
        const outcomesEmpty = !Array.isArray(raw.outcomes) || raw.outcomes.length === 0;

        if (outcomesEmpty && saved.outcomes?.length)
        {
            clone.outcomes = saved.outcomes as Outcome[];
        }

        // Restore single outcome, features, and flow if available
        if (saved.outcome) clone.outcome = saved.outcome as Outcome;
        if (saved.features) clone.features = saved.features as BaseFeatures;
        if (saved.flow) clone.flow = saved.flow as Flow;

        // Preserve any free rounds info from init, otherwise fallback to saved
        if (!clone.free_rounds && saved.free_rounds)
        {
            clone.free_rounds = saved.free_rounds;
        }

        // Merge balance: keep current wallet/free_rounds but restore saved game win
        clone.balance = this.mergeBalance(raw.balance, saved.balance);

        return clone;
    }

    /** Loads the last saved spin result from storage, validating API version and age. */
    private getSavedSpin(): SavedSpin | null
    {
        const key = this.d.getSpinKey();
        const tkey = this.d.getSpinTimeKey();

        const saved = this.d.storage.get<SavedSpin>(key);
        const tsRaw = this.d.storage.get<number>(tkey);

        if (!saved || String(saved.api_version) !== '2') return null;

        const ts = Number(tsRaw);

        if (!Number.isFinite(ts)) return null;

        const age = this.d.now() - ts;

        if (age >= this.d.maxAgeMs) return null;

        return saved;
    }

    /**
     * Returns true if the init response already has an outcome or non-empty outcomes list.
     * @param raw
     */
    private hasAnyOutcome(raw: InitResp): boolean
    {
        return Boolean(raw.outcome) || (Array.isArray(raw.outcomes) && raw.outcomes.length > 0);
    }

    /**
     * Combines current and saved balances.
     * - Keeps wallet and free_rounds from the current init.
     * - Restores `game` win amount from saved spin if available.
     * @param current
     * @param saved
     */
    private mergeBalance(current?: Balance, saved?: Balance): Balance
    {
        return {
            ...(current ?? { game: 0, wallet: 0 }),
            game: saved?.game ?? current?.game ?? 0,
            free_rounds: current?.free_rounds ?? 0,
        };
    }

    /**
     * Creates a baseline shallow copy of init with `balance` and `options` cloned.
     * Prevents accidental mutation of original response objects.
     * @param raw
     */
    private cloneBaseline(raw: InitResp): InitResp
    {
        return <InitResp>{
            ...raw,
            balance: { ...(raw.balance ?? { game: 0, wallet: 0 }) },
            options: { ...(raw.options ?? {}) },
        };
    }
}
