import type { Class } from "@common/types";
import { Component, type EvalContext } from "../core/world";
import { evaluateCondition } from "../utils/conditions";
import { action, component, ComponentTag, getComponentMeta, getComponentName, tag, variable } from "../utils/decorators";
import { Stat } from "./stat";

abstract class BaseEffect extends Component<{
    target: string;        // component class ID, e.g. 'Stat'
    targetKey?: string;   // component key, e.g. 'health'
    targetField: 'value' | 'max' | 'min';
    delta: number;
    duration: number | null;   // null = permanent until removed
    gradual: boolean;          // true if delta should be applied slowly over duration
    permanent: boolean;        // true if effect should not be removed by clear()
    remaining: number | null;  // countdown in seconds; null for condition-based/permanent
    condition: string | null;  // keep effect while true; remove when it becomes false
    stacking: 'stack' | 'unique' | 'replace';
    tag: string | null;        // discriminator for stacking; null = no stacking enforcement
}> {
    constructor(opts: {
        target?: Class<Component<any>> | string,
        targetKey?: string;
        delta: number;
        targetField?: 'value' | 'max' | 'min';
        duration?: number;
        gradual?: boolean;
        permanent?: boolean;
        condition?: string;
        stacking?: 'stack' | 'unique' | 'replace';
        tag?: string;
    }) {
        super({
            target: typeof opts.target === 'string'
                ? opts.target
                : getComponentName(opts.target ?? Stat)!,
            targetKey: opts.targetKey,
            targetField: opts.targetField ?? 'value',
            delta: opts.delta,
            duration: opts.duration ?? null,
            gradual: opts.gradual ?? false,
            permanent: opts.permanent ?? false,
            remaining: opts.duration ?? null,
            condition: opts.condition ?? null,
            stacking: opts.stacking ?? 'stack',
            tag: opts.tag ?? null,
        });

        if (opts.gradual && opts.duration == null) {
            throw new Error('Effect cannot be gradual without a duration');
        }

        if (opts.duration != null && opts.duration <= 0) {
            throw new Error('Effect duration must be greater than zero');
        }

        if (opts.permanent && opts.duration != null) {
            throw new Error('Effect cannot be both permanent and have a duration');
        }
    }
}

@tag(ComponentTag.Equippable)
@component
export class Effect extends BaseEffect {
    /** True while the effect's delta is applied to the target stat. */
    @variable('.') active: boolean = false;

    override onAdd(): void {
        const { stacking, tag } = this.state;

        if (this.state.permanent && this.state.condition) {
            if (!evaluateCondition(this.state.condition, this.entity)) {
               this.entity.remove(this);
               return;
            }
        }

        if (tag != null && stacking !== 'stack') {
            const siblings = this.entity.getAll(Effect).filter(e => e !== this && e.state.tag === tag);

            if (stacking === 'unique' && siblings.length > 0) {
                // An effect with this tag is already active — discard the incoming one.
                this.entity.remove(this);
                return;
            }

            if (stacking === 'replace') {
                for (const old of siblings) {
                    this.entity.remove(old);
                }
            }
        }

        if (this.state.gradual) {
            this.active = true;
        } else {
            this.applyDelta(this.state.delta);
        }

        if (this.state.permanent) {
            this.entity.remove(this); // permanent effects are removed immediately
        }

        if (this.state.remaining || this.state.condition) {
            this.ensureEffectSystem();
        }
    }

    override onRemove(): void {
        if (!this.active) return;
        this.active = false;

        if (this.state.permanent || this.state.gradual) return;

        this.applyDelta(-this.state.delta);
        this.active = false; // applyDelta sets active to true again
    }

    @action
    reset(duration?: number): void {
        this.ensureEffectSystem();
        if (duration != null) {
            this.state.duration = duration;
        }
        this.state.remaining = this.state.duration;
        this.active = true;
    }

    /** Mark as expired. EffectSystem will remove the component, which reverses the delta. */
    @action
    clear(): void {
        this.ensureEffectSystem();
        this.state.remaining = 0;
        this.emit('expired');
    }

    update(dt: number): void {
        if (this.state.remaining != null) {
            if (this.state.remaining > 0) {
                this.state.remaining -= dt;
                if (this.state.remaining <= 0) {
                    this.clear();
                }

                if (this.state.gradual && this.state.duration) {
                    const delta = this.state.delta * dt / this.state.duration;
                    this.applyDelta(delta);
                }
            }
        } else if (this.state.condition != null) {
            if (!evaluateCondition(this.state.condition, this.entity)) {
                this.clear();
            }
        }
        // static effect (no duration, no condition): nothing to do
    }

    private applyDelta(delta: number) {
        const component = getComponentMeta(this.state.target);
        if (component) {
            const stat = this.entity.get(component.ctor, this.state.targetKey);
            if (stat instanceof Stat) {
                stat.applyModifier(delta, this.state.targetField);
                this.active = true;
            }
        }
    }

    private async ensureEffectSystem() {
        const { EffectSystem } = await import('@common/rpg/systems/effect');

        if (!this.world.hasSystem(EffectSystem)) {
            this.world.addSystem(new EffectSystem());
        }
    }
}

@component
export class EffectTemplate extends BaseEffect {
}
