/** Configuration accepted by {@link BalanceModel}. */
export interface BalanceModelConfig
{
    /** Starting balance value. */
    initialBalance?: number;
}

const NEGATIVE_BALANCE_ERROR = 'Balance cannot be negative.';
const ADD_AMOUNT_ERROR = 'Amount to add cannot be negative.';
const SUBTRACT_AMOUNT_ERROR = 'Amount to subtract must be greater than zero.';
const INSUFFICIENT_BALANCE_ERROR = 'Insufficient balance.';
const INITIAL_BALANCE_ERROR = 'Initial balance must be a finite number.';
const BALANCE_VALUE_ERROR = 'Balance must be a finite number.';

/** Tracks and validates the player balance. */
export default class BalanceModel
{
    private currentBalance: number;

    constructor();
    constructor(initialBalance: number);
    constructor(config: BalanceModelConfig);
    constructor(initialBalanceOrConfig: number | BalanceModelConfig = 0)
    {
        const initialBalance = BalanceModel.resolveInitialBalance(initialBalanceOrConfig);

        this.currentBalance = initialBalance;
    }

    /** Current balance accessor for compatibility with existing consumers. */
    public getBalance(): number
    {
        return this.currentBalance;
    }

    /** Alias getter for typed callers. */
    public get balance(): number
    {
        return this.currentBalance;
    }

    /**
     * Sets the balance to a new value after validation.
     * @param newBalance
     */
    public setBalance(newBalance: number): void
    {
        BalanceModel.assertFinite(newBalance, BALANCE_VALUE_ERROR);

        if (newBalance < 0)
        {
            throw new Error(NEGATIVE_BALANCE_ERROR);
        }

        this.currentBalance = newBalance;
    }

    /**
     * Adds a non-negative amount to the balance.
     * @param amount Zero keeps the current balance.
     */
    public add(amount: number): void
    {
        const normalizedAmount = BalanceModel.requireNonNegativeAmount(amount, ADD_AMOUNT_ERROR);

        this.currentBalance += normalizedAmount;
    }

    /**
     * Subtracts a positive amount from the balance when affordable.
     * @param amount
     */
    public subtract(amount: number): void
    {
        const normalizedAmount = BalanceModel.requirePositiveAmount(amount, SUBTRACT_AMOUNT_ERROR);

        if (!this.canAfford(normalizedAmount))
        {
            throw new Error(INSUFFICIENT_BALANCE_ERROR);
        }

        this.currentBalance -= normalizedAmount;
    }

    /**
     * Checks whether the balance covers the desired amount.
     * @param amount
     */
    public canAfford(amount: number): boolean
    {
        if (typeof amount !== 'number' || !Number.isFinite(amount))
        {
            return false;
        }

        return this.currentBalance >= amount;
    }

    private static resolveInitialBalance(value: number | BalanceModelConfig): number
    {
        const initial = typeof value === 'number'
            ? value
            : value?.initialBalance ?? 0;

        BalanceModel.assertFinite(initial, INITIAL_BALANCE_ERROR);

        if (initial < 0)
        {
            throw new Error(NEGATIVE_BALANCE_ERROR);
        }

        return initial;
    }

    private static requirePositiveAmount(amount: number, errorMessage: string): number
    {
        if (typeof amount !== 'number' || !Number.isFinite(amount) || amount <= 0)
        {
            throw new Error(errorMessage);
        }

        return amount;
    }

    private static requireNonNegativeAmount(amount: number, errorMessage: string): number
    {
        if (typeof amount !== 'number' || !Number.isFinite(amount) || amount < 0)
        {
            throw new Error(errorMessage);
        }

        return amount;
    }

    private static assertFinite(value: number, errorMessage: string): void
    {
        if (typeof value !== 'number' || !Number.isFinite(value))
        {
            throw new Error(errorMessage);
        }
    }
}
