/** AutoGameModel orchestrates auto-spin state and stop conditions. */
export interface AutoGameConfig {
    issued?: number;
    startingBalance?: number;
    advanced?: AutoGameAdvancedSettings;
}

/** Optional advanced rules for auto spins. */
export interface AutoGameAdvancedSettings {
    spinsExceedLimit?: boolean;
    anyWin?: boolean;
    bonusGameWon?: boolean;
    singleWin?: Partial<AutoGameSingleWinConfig>;
    balanceIncrease?: Partial<AutoGameBalanceChangeConfig>;
    balanceDecrease?: Partial<AutoGameBalanceChangeConfig>;
}

/** Base structure for spin counting. */
export interface SpinCount {
    issued: number;
    current: number;
}

interface AutoGameSingleWinConfig {
    enabled: boolean;
    threshold: number;
}

interface AutoGameBalanceChangeConfig {
    enabled: boolean;
    threshold: number;
}

interface AutoGameEvaluationContext {
    spinCount: SpinCount;
    balanceChange: number;
    currentWin: number;
    bonusGameWon: boolean;
}

/** Base contract for stop conditions. */
abstract class StopCondition {
    constructor(public isEnabled = false) {}
    public abstract isMet(context: AutoGameEvaluationContext): boolean;
}

/** Stops auto spins when issued spins are exceeded. */
export class SpinsExceedLimit extends StopCondition {
    public override isMet(context: AutoGameEvaluationContext): boolean {
        return this.isEnabled && context.spinCount.current > context.spinCount.issued;
    }
}

/** Stops auto spins on any win. */
export class AnyWinCondition extends StopCondition {
    public override isMet(context: AutoGameEvaluationContext): boolean {
        return this.isEnabled && context.currentWin > 0;
    }
}

/** Stops auto spins when a bonus game is won. */
export class BonusGameWonCondition extends StopCondition {
    public override isMet(context: AutoGameEvaluationContext): boolean {
        return this.isEnabled && context.bonusGameWon;
    }
}

/** Stops auto spins when a single win exceeds the configured threshold. */
export class SingleWinExceedsCondition extends StopCondition {
    constructor(isEnabled: boolean, public threshold: number) {
        super(isEnabled);
    }

    public override isMet(context: AutoGameEvaluationContext): boolean {
        return this.isEnabled && context.currentWin > this.threshold;
    }
}

/** Stops auto spins when the balance increases too much. */
export class BalanceIncreaseCondition extends StopCondition {
    constructor(isEnabled: boolean, public threshold: number) {
        super(isEnabled);
    }

    public override isMet(context: AutoGameEvaluationContext): boolean {
        return this.isEnabled && context.balanceChange >= this.threshold;
    }
}

/** Stops auto spins when the balance decreases too much. */
export class BalanceDecreaseCondition extends StopCondition {
    constructor(isEnabled: boolean, public threshold: number) {
        super(isEnabled);
    }

    public override isMet(context: AutoGameEvaluationContext): boolean {
        return this.isEnabled && context.balanceChange <= -this.threshold;
    }
}

/** Names of all supported stop conditions. */
type ConditionName =
  | 'spinsExceedLimit'
  | 'anyWin'
  | 'bonusGameWon'
  | 'singleWin'
  | 'balanceIncrease'
  | 'balanceDecrease';

/** Mapping from name to concrete condition class. */
type ConditionByName = {
    spinsExceedLimit: SpinsExceedLimit;
    anyWin: AnyWinCondition;
    bonusGameWon: BonusGameWonCondition;
    singleWin: SingleWinExceedsCondition;
    balanceIncrease: BalanceIncreaseCondition;
    balanceDecrease: BalanceDecreaseCondition;
};

/** Evaluates and manages registered stop conditions (type-safe). */
class StopConditions {
    private readonly conditions = new Map<ConditionName, ConditionByName[ConditionName]>();

    public addCondition<N extends ConditionName>(name: N, condition: ConditionByName[N]): void {
        this.conditions.set(name, condition);
    }

    public removeCondition(name: ConditionName): void {
        this.conditions.delete(name);
    }

    /**
     * Type-safe update of condition properties.
     * You can change `isEnabled` for any condition and only properties that exist
     * on the specific condition class (e.g., `threshold` on thresholded conditions).
     */
    public setConditionProperties<N extends ConditionName>(
      name: N,
      properties:
        & Partial<Omit<ConditionByName[N], keyof StopCondition>>
        & Partial<Pick<StopCondition, 'isEnabled'>>
    ): void {
        const condition = this.conditions.get(name);
        if (!condition) {
            throw new Error(`Stop condition "${name}" does not exist.`);
        }
        Object.assign(condition, properties);
    }

    public conditionsMet(context: AutoGameEvaluationContext): boolean {
        for (const condition of this.conditions.values()) {
            if (condition.isMet(context)) {
                return true;
            }
        }
        return false;
    }
}

export default class AutoGameModel {
    public readonly spinCount: SpinCount;
    public readonly startingBalance: number;

    private readonly stopConditions: StopConditions;

    constructor({ issued = 0, startingBalance = 0, advanced = {} }: AutoGameConfig = {}) {
        this.spinCount = {
            issued,
            current: 0,
        };

        this.startingBalance = startingBalance;
        this.stopConditions = new StopConditions();

        this.registerDefaultConditions(advanced);
    }

    /** Checks whether there are spins left to play automatically. */
    public isActive(): boolean {
        return this.spinCount.current < this.spinCount.issued;
    }

    /** Registers the start of a spin. */
    public startSpin(): void {
        if (!this.isActive()) {
            if (this.spinCount.current >= this.spinCount.issued) {
                throw new Error('You have reached the limit of issued spins.');
            }
            throw new Error('Auto game is not active.');
        }

        this.spinCount.current++;
    }

    /**
     * Finalises a spin and evaluates stop conditions.
     * @param currentBalance
     * @param currentWin
     * @param bonusGameWon
     */
    public endSpin(currentBalance: number, currentWin: number, bonusGameWon: boolean): void {
        const evaluationContext = this.createEvaluationContext(currentBalance, currentWin, bonusGameWon);

        if (this.stopConditions.conditionsMet(evaluationContext)) {
            this.spinCount.issued = 0;
        }
    }

    private registerDefaultConditions(advanced: AutoGameAdvancedSettings): void {
        this.stopConditions.addCondition(
          'spinsExceedLimit',
          new SpinsExceedLimit(advanced.spinsExceedLimit ?? true),
        );
        this.stopConditions.addCondition(
          'anyWin',
          new AnyWinCondition(advanced.anyWin ?? false),
        );
        this.stopConditions.addCondition(
          'bonusGameWon',
          new BonusGameWonCondition(advanced.bonusGameWon ?? false),
        );
        this.stopConditions.addCondition(
          'singleWin',
          new SingleWinExceedsCondition(
            advanced.singleWin?.enabled ?? false,
            advanced.singleWin?.threshold ?? 0,
          ),
        );
        this.stopConditions.addCondition(
          'balanceIncrease',
          new BalanceIncreaseCondition(
            advanced.balanceIncrease?.enabled ?? false,
            advanced.balanceIncrease?.threshold ?? 0,
          ),
        );
        this.stopConditions.addCondition(
          'balanceDecrease',
          new BalanceDecreaseCondition(
            advanced.balanceDecrease?.enabled ?? false,
            advanced.balanceDecrease?.threshold ?? 0,
          ),
        );
    }

    private createEvaluationContext(
      currentBalance: number,
      currentWin: number,
      bonusGameWon: boolean,
    ): AutoGameEvaluationContext {
        return {
            spinCount: this.spinCount,
            balanceChange: currentBalance - this.startingBalance,
            currentWin,
            bonusGameWon,
        };
    }
}
