import { AttackType, EnemyType, IEnemyConfig, IReinforcementRule } from '../data/Interfaces';

/**
 * Enemy AI base class + four concrete subclasses (req 6.1-6.7).
 *
 * Each enemy is modelled as a tiny state machine that ticks on `update(dt)`
 * and emits an `IEnemyAction[]` the level manager then spawns into the world
 * (bullets / smoke bombs / fireballs / sword swings).
 *
 * Enemies outside the camera's culling rect can be frozen by simply skipping
 * `update()` — see `EnemyManager.update()` below (requirement 6.7 / 18.5).
 */

export interface IEnemyAction {
    kind: 'fire_bullet' | 'melee_swing' | 'spawn_item' | 'drop_item';
    attackType?: AttackType;
    /** Origin of the projectile, world coords. */
    originX?: number;
    originY?: number;
    /** Velocity of the projectile. */
    velX?: number;
    velY?: number;
    /** For drop_item only — item id + world coords. */
    itemId?: string;
}

export interface IPlayerSense {
    x: number;
    y: number;
    isGrounded: boolean;
}

export interface IEnemyAABB {
    x: number;
    y: number;
    w: number;
    h: number;
}

export interface IEnemyUpdateCtx {
    dtSec: number;
    nowMs: number;
    player: IPlayerSense;
}

export abstract class EnemyAIBase {
    public readonly type: EnemyType;
    public pos: { x: number; y: number };
    public alive = true;
    public hp: number;
    protected cooldownSec = 0;
    protected readonly cfg: IEnemyConfig;

    constructor(cfg: IEnemyConfig, spawnX: number, spawnY: number) {
        this.cfg = cfg;
        this.type = cfg.id;
        this.pos = { x: spawnX, y: spawnY };
        this.hp = cfg.hp;
    }

    public abstract update(ctx: IEnemyUpdateCtx): IEnemyAction[];

    public get aabb(): IEnemyAABB {
        return { x: this.pos.x, y: this.pos.y, w: this.cfg.size.w, h: this.cfg.size.h };
    }

    protected decrementCooldown(dtSec: number): boolean {
        this.cooldownSec -= dtSec;
        if (this.cooldownSec > 0) return false;
        this.cooldownSec = this.cfg.attackIntervalSec;
        return true;
    }
}

// ---------- Qing Ren (req 6.1) --------------------------------------------
export class QingRenAI extends EnemyAIBase {
    public update(ctx: IEnemyUpdateCtx): IEnemyAction[] {
        if (!this.decrementCooldown(ctx.dtSec)) return [];
        const dx = ctx.player.x - this.pos.x;
        const horizontalDistance = Math.abs(dx);
        const direction = dx >= 0 ? 1 : -1;
        if (horizontalDistance < 64) {
            return [{ kind: 'melee_swing', attackType: 'sword' }];
        }
        return [
            {
                kind: 'fire_bullet',
                attackType: 'shuriken',
                originX: this.pos.x,
                originY: this.pos.y,
                velX: 240 * direction,
                velY: 0,
            },
        ];
    }
}

// ---------- Chi Ren (req 6.2-6.4) -----------------------------------------
export class ChiRenAI extends EnemyAIBase {
    private interceptCooldown = 0;

    public update(ctx: IEnemyUpdateCtx): IEnemyAction[] {
        // Patrol horizontally toward the player at 120px/s (req 6.2).
        const dx = ctx.player.x - this.pos.x;
        const direction = dx >= 0 ? 1 : -1;
        this.pos.x += direction * this.cfg.moveSpeed * ctx.dtSec;

        // Proactive intercept jump when player stands still within vision (req 6.3).
        if (this.interceptCooldown > 0) this.interceptCooldown -= ctx.dtSec;
        const playerIdle = Math.abs(ctx.player.x - this.pos.x) < 200 && ctx.player.isGrounded;
        if (playerIdle && this.interceptCooldown <= 0) {
            this.interceptCooldown = 3;
            // Intercept arc: +X velocity + upward bounce, treated as position warp.
            this.pos.y += 48;
        }

        if (!this.decrementCooldown(ctx.dtSec)) return [];
        return [
            {
                kind: 'fire_bullet',
                attackType: 'smoke_bomb',
                originX: this.pos.x,
                originY: this.pos.y,
                velX: 140 * direction,
                velY: 0,
            },
        ];
    }
}

// ---------- Hei Ren (req 6.5) ---------------------------------------------
export class HeiRenAI extends EnemyAIBase {
    private hasDroppedMagicFlute = false;

    public update(ctx: IEnemyUpdateCtx): IEnemyAction[] {
        if (!this.decrementCooldown(ctx.dtSec)) return [];
        const dx = ctx.player.x - this.pos.x;
        const direction = dx >= 0 ? 1 : -1;
        if (Math.abs(dx) < 96) {
            return [{ kind: 'melee_swing', attackType: 'sword' }];
        }
        return [
            {
                kind: 'fire_bullet',
                attackType: 'shuriken',
                originX: this.pos.x,
                originY: this.pos.y,
                velX: 200 * direction,
                velY: 0,
            },
        ];
    }

    /** Called by EnemyManager on death — yields at most one magic flute. */
    public onKilled(): IEnemyAction[] {
        if (this.hasDroppedMagicFlute) return [];
        this.hasDroppedMagicFlute = true;
        return [{ kind: 'drop_item', itemId: 'mo_di', originX: this.pos.x, originY: this.pos.y }];
    }
}

// ---------- Yao Fang (req 6.6) --------------------------------------------
export class YaoFangAI extends EnemyAIBase {
    public update(ctx: IEnemyUpdateCtx): IEnemyAction[] {
        if (!this.decrementCooldown(ctx.dtSec)) return [];
        const dx = ctx.player.x - this.pos.x;
        const direction = dx >= 0 ? 1 : -1;
        return [
            {
                kind: 'fire_bullet',
                attackType: 'fireball',
                originX: this.pos.x,
                originY: this.pos.y,
                velX: 260 * direction,
                velY: 0,
            },
        ];
    }
}

// ---------- Manager with camera-culling (req 6.7) -------------------------

export interface ICullingRect {
    leftX: number;
    rightX: number;
    topY: number;
    bottomY: number;
}

export class EnemyManager {
    private readonly enemies: EnemyAIBase[] = [];

    public spawn(enemy: EnemyAIBase): void {
        this.enemies.push(enemy);
    }

    public get all(): ReadonlyArray<EnemyAIBase> {
        return this.enemies;
    }

    /**
     * Update all live enemies that intersect `cull` (with optional margin).
     * Returns the concatenated list of actions emitted so the caller
     * (LevelMgr) can instantiate projectiles, drops, etc.
     *
     * @param cullingMargin Extra pixels beyond `cull` to still activate enemies.
     *                      Used for reinforcements that spawn just outside the screen.
     */
    public update(dtSec: number, nowMs: number, player: IPlayerSense, cull: ICullingRect, cullingMargin = 0): IEnemyAction[] {
        const actions: IEnemyAction[] = [];
        for (const e of this.enemies) {
            if (!e.alive) continue;
            if (!this.inside(e, cull, cullingMargin)) continue;
            const ctx: IEnemyUpdateCtx = { dtSec, nowMs, player };
            actions.push(...e.update(ctx));
        }
        return actions;
    }

    public kill(enemy: EnemyAIBase): IEnemyAction[] {
        enemy.alive = false;
        if (enemy instanceof HeiRenAI) return enemy.onKilled();
        return [];
    }

    public clear(): void {
        this.enemies.length = 0;
    }

    private inside(e: EnemyAIBase, cull: ICullingRect, margin = 0): boolean {
        return (
            e.pos.x + e.aabb.w / 2 >= cull.leftX - margin &&
            e.pos.x - e.aabb.w / 2 <= cull.rightX + margin &&
            e.pos.y + e.aabb.h / 2 >= cull.bottomY - margin &&
            e.pos.y - e.aabb.h / 2 <= cull.topY + margin
        );
    }
}

// ---------- Reinforcement Scheduler (dynamic edge spawns) -----------------

/** An enemy spawned by the reinforcement scheduler, with a jump-in arc. */
export interface IReinforcementSpawn {
    enemy: EnemyAIBase;
    /** The edge the enemy is jumping in from. */
    edge: 'left' | 'right';
}

/**
 * Manages periodic reinforcement waves — enemies that jump in from screen
 * edges even when the player is standing still. Each rule specifies an
 * enemy type, interval, count, and which edge(s) they appear from.
 *
 * Usage: call `tick()` every frame. It returns newly-spawned enemies that
 * the caller must register with `EnemyManager.spawn()` and create visual
 * nodes for.
 */
export class ReinforcementScheduler {
    private readonly timers = new Map<number, number>();
    private readonly totals = new Map<number, number>();

    constructor(private readonly rules: IReinforcementRule[]) {}

    /**
     * Tick the scheduler. Returns enemies that should be spawned this frame.
     * The caller is responsible for:
     *   1. Passing the new enemies to `EnemyManager.spawn()`
     *   2. Creating visual nodes for them
     *   3. Calling `EnemyManager.update()` as usual
     *
     * @param dtSec Frame delta in seconds.
     * @param elapsedSec Total elapsed seconds since level start.
     * @param cull Current camera culling rect (used to determine edge positions).
     * @param groundY Ground Y in physics coords (feet surface).
     * @param enemyCfgFactory A function that returns the IEnemyConfig for a given EnemyType.
     */
    public tick(
        dtSec: number,
        elapsedSec: number,
        cull: ICullingRect,
        groundY: number,
        enemyCfgFactory: (type: EnemyType) => { moveSpeed: number; attackIntervalSec: number; size: { w: number; h: number } },
    ): IReinforcementSpawn[] {
        const spawns: IReinforcementSpawn[] = [];

        for (let i = 0; i < this.rules.length; i++) {
            const rule = this.rules[i];

            // Check delay
            if (rule.delaySec && elapsedSec < rule.delaySec) continue;

            // Check max total
            const total = this.totals.get(i) ?? 0;
            if (rule.maxTotal && rule.maxTotal > 0 && total >= rule.maxTotal) continue;

            // Accumulate timer
            const timer = this.timers.get(i) ?? 0;
            const newTimer = timer + dtSec;
            this.timers.set(i, newTimer);

            if (newTimer < rule.intervalSec) continue;

            // Reset timer (keep remainder for smoother intervals)
            this.timers.set(i, newTimer - rule.intervalSec);

            // Determine edge(s)
            const edges: Array<'left' | 'right'> = rule.edge === 'both'
                ? (Math.random() < 0.5 ? ['left'] : ['right'])
                : [rule.edge];

            for (const edge of edges) {
                for (let j = 0; j < rule.count; j++) {
                    const cfg = enemyCfgFactory(rule.type);
                    // Spawn position: just outside the culling rect on the chosen edge.
                    // Moving enemies (moveSpeed > 0) start farther out and walk in;
                    // stationary enemies (YaoFang) start just inside the visible edge.
                    const margin = cfg.moveSpeed > 0 ? 60 : -30; // negative = inside screen
                    const spawnX = edge === 'left'
                        ? cull.leftX - margin - j * 50
                        : cull.rightX + margin + j * 50;
                    const spawnY = groundY + cfg.size.h / 2;

                    const enemy = createReinforcementEnemy(rule.type, cfg, spawnX, spawnY, edge);
                    spawns.push({ enemy, edge });
                    this.totals.set(i, (this.totals.get(i) ?? 0) + 1);
                }
            }
        }

        return spawns;
    }

    /** Reset all timers and counters (e.g. on level restart). */
    public reset(): void {
        this.timers.clear();
        this.totals.clear();
    }
}

/**
 * Create an enemy AI instance for a reinforcement spawn.
 * Reuses the same AI classes as static spawns. The enemy spawns at ground
 * level on the chosen edge; moving enemies (ChiRen) will walk into the
 * screen automatically, while stationary enemies (YaoFang) are placed
 * just inside the visible edge so they're immediately active.
 */
function createReinforcementEnemy(
    type: EnemyType,
    cfg: { moveSpeed: number; attackIntervalSec: number; size: { w: number; h: number } },
    spawnX: number,
    spawnY: number,
    _edge: 'left' | 'right',
): EnemyAIBase {
    const fullCfg = {
        id: type,
        displayName: type,
        size: cfg.size,
        moveSpeed: cfg.moveSpeed,
        attackIntervalSec: cfg.attackIntervalSec,
        attacks: [] as never[],
        hp: type === EnemyType.HeiRen ? 2 : 1,
    };

    switch (type) {
        case EnemyType.ChiRen:   return new ChiRenAI(fullCfg, spawnX, spawnY);
        case EnemyType.HeiRen:   return new HeiRenAI(fullCfg, spawnX, spawnY);
        case EnemyType.YaoFang:  return new YaoFangAI(fullCfg, spawnX, spawnY);
        case EnemyType.QingRen:
        default:                 return new QingRenAI(fullCfg, spawnX, spawnY);
    }
}
