import { StateMachine, Cooldown, type Vec2 } from '@rpg/engine';
import type { EnemyKindDef } from '../../data/enemies';

/**
 * Чистый ИИ «пепельного сгустка» поверх движкового StateMachine.
 * Сенсоры входят (дистанция/направление до героя, видимость, шум, патруль),
 * намерения выходят (куда идти, когда бить/плевать). Без Pixi — тестируется
 * в Vitest.
 *
 * Состояния: спящий враг (dormant, старт без патруля) -> rise -> chase ->
 * windup -> attack -> recover -> chase; патрульный враг: patrol <-> wary ->
 * chase; отступление: chase -> flee (низкое hp) / -> return (потерял героя) ->
 * patrol | dormant; sleeping (от низкого резонанса), dead.
 *
 * Шум слышен бодрому врагу от уровня NOISE_HEAR_LEVEL (в радиусе hearRadius —
 * фильтрует CombatWorld); спящих будит только уровень >= 0.7 (гулкий удар).
 */
export type EnemyIntent =
    | { type: 'idle' }
    | { type: 'move'; dir: Vec2; run?: boolean }
    | { type: 'strike' }
    | { type: 'shoot'; dir: Vec2 };

/** Варианты намерений с направлением — для типизации переиспользуемых констант. */
type MoveIntent = Extract<EnemyIntent, { type: 'move' }>;
type ShootIntent = Extract<EnemyIntent, { type: 'shoot' }>;

export interface EnemySenses {
    /** Дистанция до героя, мировых юнитов (1 юнит = 1 тайл). */
    dist: number;
    /** Нормализованное направление к герою. */
    dirToPlayer: Vec2;
    /** Позиция врага (для хождения по патрульным точкам и к дому). */
    pos?: Vec2;
    /** Герой в прямой видимости (LOS; по умолчанию true — старые сенсоры). */
    canSee?: boolean;
    /** Последний шум в радиусе слуха (уже отфильтрован по hearRadius). */
    noise?: { origin: Vec2; level: number } | null;
    /** Точки патруля, мировые юниты (пусто — враг без патруля). */
    patrolPoints?: Vec2[];
    /** Точка спавна — «дом», к которому враг отступает, юниты. */
    home?: Vec2;
    /** Доля hp (0..1; по умолчанию 1). */
    hpFraction?: number;
}

/** Шум от этого уровня слышит бодрый враг (спящих будит CombatWorld от 0.7). */
export const NOISE_HEAR_LEVEL = 0.35;

/** Шум от этого уровня будит спящих (гулкий удар; см. CombatWorld.noise). */
export const NOISE_WAKE_LEVEL = 0.7;

/** Легальные состояния автомата (инвариант enemy-state-legal). */
export const ENEMY_STATES = [
    'dormant',
    'rise',
    'patrol',
    'wary',
    'chase',
    'windup',
    'attack',
    'recover',
    'hurt',
    'flee',
    'return',
    'sleeping',
    'dead'
] as const;

/** Дальность, на которой считается «дошёл до точки» (юниты). */
const ARRIVE_DIST = 0.35;

/**
 * Переиспользуемые намерения и вектор направления: update() вызывается на
 * каждый тик каждого врага — новые объекты там не создаются. Потребитель
 * (CombatWorld) читает intent сразу, между тиками не хранит.
 */
const INTENT_IDLE: EnemyIntent = { type: 'idle' };
const INTENT_STRIKE: EnemyIntent = { type: 'strike' };
const INTENT_MOVE: MoveIntent = { type: 'move', dir: { x: 0, y: 0 } };
const INTENT_MOVE_RUN: MoveIntent = { type: 'move', dir: { x: 0, y: 0 }, run: true };
const INTENT_SHOOT: ShootIntent = { type: 'shoot', dir: { x: 0, y: 0 } };
const MOVE_DIR: Vec2 = { x: 0, y: 0 };

export class EnemyBrain {
    readonly sm = new StateMachine();
    private attackCd: Cooldown;
    private sleepTimer = 0;
    private hurtTimer = 0;
    /** Секунд без прямой видимости в погоне. */
    private lostTimer = 0;
    /** Таймер настороженности и точка, к которой идём проверять. */
    private waryTimer = 0;
    private waryTarget: Vec2 | null = null;
    /** Патруль: индекс точки (ping-pong) и пауза на точке, сек. */
    private patrolIndex = 0;
    private patrolDir = 1;
    private patrolPause = 0;

    constructor(
        readonly kind: EnemyKindDef,
        private patrol?: { points: Vec2[]; pauseSec: number }
    ) {
        this.attackCd = new Cooldown(kind.attackCooldown);

        for (const s of ENEMY_STATES) this.sm.add(s, {});

        // Обычные переходы
        this.sm.transition('dormant', 'wake', 'rise');
        this.sm.transition('rise', 'done', 'chase');
        // Патруль: увидел — погоня, услышал шум — насторожился.
        this.sm.transition('patrol', 'see', 'chase');
        this.sm.transition('patrol', 'noise', 'wary');
        this.sm.transition('wary', 'see', 'chase');
        this.sm.transition('wary', 'lost', 'return');
        this.sm.transition('chase', 'inRange', 'windup');
        this.sm.transition('chase', 'exhausted', 'sleeping');
        this.sm.transition('chase', 'lost', 'return');
        this.sm.transition('chase', 'flee', 'flee');
        this.sm.transition('flee', 'calm', 'wary');
        this.sm.transition('return', 'home', this.patrol ? 'patrol' : 'dormant');
        this.sm.transition('return', 'see', 'chase');
        this.sm.transition('windup', 'strike', 'attack');
        this.sm.transition('attack', 'done', 'recover');
        this.sm.transition('recover', 'done', 'chase');
        this.sm.transition('hurt', 'done', 'chase');
        this.sm.transition('sleeping', 'wake', 'rise');

        // Из любого состояния: урон и смерть (кроме dead — повторный вход игнорируется)
        this.sm.transitionAny('hurt', 'hurt');
        this.sm.transitionAny('die', 'dead');

        this.sm.change(this.patrol ? 'patrol' : 'dormant');
    }

    /** Гулкий звук рядом — проснуться (только для спящих; уровень фильтрует CombatWorld). */
    hearLoud(): void {
        if (this.sm.current === 'dormant' || this.sm.current === 'sleeping') {
            this.sm.handleEvent('wake');
        }
    }

    /** Низкий резонанс: усыпить на seconds (мёртвых не трогает). */
    putToSleep(seconds: number): void {
        if (this.sm.current === 'dead') return;
        this.sm.change('sleeping');
        this.sleepTimer = seconds;
    }

    /** Получил урон: короткий стан (не выбивает из dead). */
    hurt(): void {
        if (this.sm.current === 'dead') return;
        this.hurtTimer = 0.25;
        this.sm.handleEvent('hurt');
    }

    /** Умер. Возвращает true, если умер именно сейчас. */
    die(): boolean {
        if (this.sm.current === 'dead') return false;
        this.sm.handleEvent('die');
        return true;
    }

    get state(): string | null {
        return this.sm.current;
    }

    get asleep(): boolean {
        return this.sm.current === 'sleeping' || this.sm.current === 'dormant';
    }

    get dead(): boolean {
        return this.sm.current === 'dead';
    }

    /** Тик ИИ: сенсоры внутрь, намерение наружу. */
    update(dt: number, senses: EnemySenses): EnemyIntent {
        this.sm.update(dt);
        this.attackCd.update(dt);

        const canSee = senses.canSee ?? true;
        const aggro = this.kind.aggroRange ?? 4;

        // Таймеры поверх автомата
        if (this.hurtTimer > 0) {
            this.hurtTimer -= dt;
            if (this.hurtTimer <= 0) this.sm.handleEvent('done');
            return INTENT_IDLE;
        }
        if (this.sm.current === 'sleeping') {
            this.sleepTimer -= dt;
            if (this.sleepTimer <= 0) this.sm.handleEvent('wake');
            return INTENT_IDLE;
        }
        if (this.sm.current === 'rise') {
            // Подъём из пепла: 0.6 с, потом в погоню
            if (this.sm.time >= 0.6) this.sm.handleEvent('done');
            return INTENT_IDLE;
        }

        switch (this.sm.current) {
            case 'patrol':
                return this.updatePatrol(dt, senses, canSee, aggro);
            case 'wary':
                return this.updateWary(dt, senses, canSee, aggro);
            case 'chase':
                return this.updateChase(dt, senses, canSee, aggro);
            case 'flee':
                return this.updateFlee(senses);
            case 'return':
                return this.updateReturn(senses, canSee, aggro);
            case 'windup': {
                if (this.sm.time >= this.kind.windup) {
                    this.sm.handleEvent('strike');
                    this.attackCd.triggerForce();
                    if (this.kind.keepDistance !== undefined) {
                        INTENT_SHOOT.dir.x = senses.dirToPlayer.x;
                        INTENT_SHOOT.dir.y = senses.dirToPlayer.y;
                        return INTENT_SHOOT;
                    }
                    return INTENT_STRIKE;
                }
                return INTENT_IDLE;
            }
            case 'attack': {
                if (this.sm.time >= Math.max(this.kind.recover, 0.3)) {
                    this.sm.handleEvent('done');
                }
                return INTENT_IDLE;
            }
            case 'recover': {
                if (this.sm.time >= this.kind.recover) this.sm.handleEvent('done');
                return INTENT_IDLE;
            }
            default:
                return INTENT_IDLE;
        }
    }

    /** Патруль: ход по точкам туда-обратно; герой/шум вырывают из круга. */
    private updatePatrol(dt: number, senses: EnemySenses, canSee: boolean, aggro: number): EnemyIntent {
        if (this.checkSee(senses, canSee, aggro)) return INTENT_IDLE;
        if (this.checkNoise(senses)) return INTENT_IDLE;

        const points = this.patrol?.points ?? senses.patrolPoints ?? [];
        if (points.length === 0) return INTENT_IDLE;
        if (this.patrolPause > 0) {
            this.patrolPause -= dt;
            return INTENT_IDLE;
        }
        if (!this.dirInto(MOVE_DIR, senses.pos, points[this.patrolIndex]!)) {
            // Точка достигнута: пауза и разворот на следующую (ping-pong).
            this.patrolPause = this.patrol?.pauseSec ?? 0;
            this.advancePatrolIndex(points.length);
            return INTENT_IDLE;
        }
        return this.moveIntentFrom(MOVE_DIR, false);
    }

    /** Настороженность: идём к источнику шума, осматриваемся warySec — и домой. */
    private updateWary(dt: number, senses: EnemySenses, canSee: boolean, aggro: number): EnemyIntent {
        if (this.checkSee(senses, canSee, aggro)) return INTENT_IDLE;
        if (this.checkNoise(senses)) return INTENT_IDLE; // свежий шум обновляет цель и таймер

        this.waryTimer -= dt;
        if (this.waryTimer <= 0) {
            this.sm.handleEvent('lost');
            return INTENT_IDLE;
        }
        if (!this.waryTarget || !this.dirInto(MOVE_DIR, senses.pos, this.waryTarget)) return INTENT_IDLE;
        return this.moveIntentFrom(MOVE_DIR, false);
    }

    /** Погоня: потерял героя дольше deaggroSec — домой; мало hp — прочь. */
    private updateChase(dt: number, senses: EnemySenses, canSee: boolean, aggro: number): EnemyIntent {
        if (canSee) {
            this.lostTimer = 0;
        } else {
            this.lostTimer += dt;
            if (this.lostTimer >= (this.kind.deaggroSec ?? 2.5)) {
                this.sm.handleEvent('lost');
                return INTENT_IDLE;
            }
        }
        const fleeAt = this.kind.fleeBelowHpFraction ?? 0;
        if (fleeAt > 0 && (senses.hpFraction ?? 1) <= fleeAt) {
            this.sm.handleEvent('flee');
            return INTENT_IDLE;
        }
        if (senses.dist > aggro * 1.5) {
            // Убежал слишком далеко даже с LOS — не гоняемся по всей карте.
            this.sm.handleEvent('lost');
            return INTENT_IDLE;
        }

        const inRange = senses.dist <= this.kind.attackRange;
        if (inRange && this.kind.keepDistance === undefined && this.attackCd.ready) {
            // контактный (ползун): без замаха
            if (this.kind.windup === 0) {
                this.attackCd.triggerForce();
                return INTENT_STRIKE;
            }
            this.sm.handleEvent('inRange');
            return INTENT_IDLE;
        }
        // Плевун держит дистанцию
        if (this.kind.keepDistance !== undefined) {
            if (senses.dist < this.kind.keepDistance - 0.4) {
                return this.moveIntentFrom(negate(MOVE_DIR, senses.dirToPlayer), false);
            }
            if (senses.dist <= this.kind.attackRange && this.attackCd.ready) {
                this.sm.handleEvent('inRange');
                return INTENT_IDLE;
            }
            if (senses.dist > this.kind.keepDistance + 0.4) {
                return this.moveIntentFrom(senses.dirToPlayer, false);
            }
            return INTENT_IDLE;
        }
        return this.moveIntentFrom(senses.dirToPlayer, false);
    }

    /** Отступление: прочь от героя бегом, пока он не отстал на fleeRange. */
    private updateFlee(senses: EnemySenses): EnemyIntent {
        if (senses.dist >= (this.kind.fleeRange ?? 4)) {
            // Оторвался: стоит и осматривается warySec, потом домой.
            this.waryTarget = null;
            this.waryTimer = this.kind.warySec ?? 2.5;
            this.sm.handleEvent('calm');
            return INTENT_IDLE;
        }
        return this.moveIntentFrom(negate(MOVE_DIR, senses.dirToPlayer), true);
    }

    /** Возврат к дому; на месте — снова в патруль или обратно в пепел. */
    private updateReturn(senses: EnemySenses, canSee: boolean, aggro: number): EnemyIntent {
        if (this.checkSee(senses, canSee, aggro)) return INTENT_IDLE;
        if (this.checkNoise(senses)) return INTENT_IDLE;
        if (senses.home) {
            if (!this.dirInto(MOVE_DIR, senses.pos, senses.home)) {
                this.lostTimer = 0;
                this.sm.handleEvent('home');
            } else {
                return this.moveIntentFrom(MOVE_DIR, false);
            }
        }
        return INTENT_IDLE;
    }

    /** Переход «увидел героя» (LOS + aggroRange); true — событие отправлено. */
    private checkSee(senses: EnemySenses, canSee: boolean, aggro: number): boolean {
        if (canSee && senses.dist <= aggro) {
            this.lostTimer = 0;
            this.sm.handleEvent('see');
            return true;
        }
        return false;
    }

    /** Переход «услышал шум» (уровень >= порога); true — событие отправлено. */
    private checkNoise(senses: EnemySenses): boolean {
        const noise = senses.noise;
        if (noise && noise.level >= NOISE_HEAR_LEVEL) {
            this.waryTarget = { ...noise.origin };
            this.waryTimer = this.kind.warySec ?? 2.5;
            this.sm.handleEvent('noise');
            return true;
        }
        return false;
    }

    /** Нормализованное направление к точке в out; false — точка достигнута (ARRIVE_DIST). */
    private dirInto(out: Vec2, from: Vec2 | undefined, target: Vec2): boolean {
        if (!from) return false;
        const dx = target.x - from.x;
        const dy = target.y - from.y;
        const d = Math.hypot(dx, dy);
        if (d < ARRIVE_DIST) return false;
        out.x = dx / d;
        out.y = dy / d;
        return true;
    }

    /** move-намерение с копией направления из dir (общий intent.dir мутируется). */
    private moveIntentFrom(dir: Vec2, run: boolean): EnemyIntent {
        const intent = run ? INTENT_MOVE_RUN : INTENT_MOVE;
        intent.dir.x = dir.x;
        intent.dir.y = dir.y;
        return intent;
    }

    private advancePatrolIndex(count: number): void {
        if (count < 2) return;
        if (this.patrolIndex + this.patrolDir >= count || this.patrolIndex + this.patrolDir < 0) {
            this.patrolDir = -this.patrolDir;
        }
        this.patrolIndex += this.patrolDir;
    }
}

/** Обратный вектор в out (без аллокации — вызывается в тике). */
function negate(out: Vec2, v: Vec2): Vec2 {
    out.x = -v.x;
    out.y = -v.y;
    return out;
}