import {
    EventBus,
    IsometricTileMap,
    World,
    worldToTile,
    worldDist,
    worldNorm,
    inCircleW,
    inConeW,
    tileToWorld,
    moveCircle,
    circleFits,
    separateCircles,
    type Entity,
    type SceneRegistry,
    type System,
    type Vec2,
    type Invariant
} from '@rpg/engine';
import {
    ENEMY_KINDS,
    withDefaults,
    type EnemyKindDef,
    type EnemyKindId,
    type EnemyPatrolDef
} from '../../data/enemies';
import { EnemyBrain, ENEMY_STATES, NOISE_HEAR_LEVEL, NOISE_WAKE_LEVEL, type EnemyIntent } from './EnemyBrain';
import { hasLineOfSight, tileOpaque } from './los';
import { PLAYER_COMBAT } from './stats';

/**
 * Боевой мир: сущности (враги, снаряды) в движковом ECS, поведение — в системах.
 * Пиксельные вьюхи держатся отдельно (CombatViews) — в ECS только данные.
 */

export interface EnemyComp {
    kind: EnemyKindDef;
    brain: EnemyBrain;
    pos: Vec2;
    hp: number;
    /** Точка спавна — «дом» для возврата после погони. */
    home: Vec2;
    /** Точки патруля, мировые юниты (пусто — враг спит в пепле). */
    patrolPoints: Vec2[];
    /** Секунды до исчезновения трупа (после смерти). */
    corpseTimer: number;
    /** Кэш прямой видимости героя: true/false и сек до пересчёта. */
    los: boolean;
    losTimer: number;
}

export interface ProjectileComp {
    pos: Vec2;
    vel: Vec2;
    damage: number;
    life: number;
}

export interface CombatDeps {
    map: IsometricTileMap;
    events: EventBus;
    /** Позиция героя (ноги), мировые юниты (1 юнит = 1 тайл). */
    getPlayerPos: () => Vec2;
    /** Герой получил урон (сцена отбросит/шейкнет/проиграет звук). */
    damagePlayer: (damage: number, from: Vec2) => void;
    /**
     * Реестр объектов сцены (опция): враги регистрируются в нём для кликов
     * и телесных коллизий — боевая система остаётся на ECS.
     */
    registry?: SceneRegistry;
}

/** Окно слышимости шума: событие живёт столько секунд для бодрых ушей. */
const NOISE_WINDOW = 0.5;
/** Пересчёт LOS раз в 3 тика (60 Гц) — видимость меняется медленнее. */
const LOS_INTERVAL = 0.05;
/** Бег (отступление) быстрее шага. */
const RUN_MULT = 1.3;

/** Движение врага: круг тела скользит вдоль стен (движковые коллизии). */
function stepWithCollision(
    map: IsometricTileMap,
    pos: Vec2,
    dir: Vec2,
    speed: number,
    radius: number,
    dt: number
): void {
    moveCircle(map, pos, { x: dir.x * speed * dt, y: dir.y * speed * dt }, radius);
}

/** Максимальный радиус тел в реестре (NPC 0.35, враги до 0.3 — с запасом). */
const BODY_PROBE = 0.5;

// ---------- системы ECS ----------

/** ИИ и движение сгустков. */
class EnemySystem implements System {
    /** Колбэк непрозрачности тайлов (карта не меняется за жизнь мира). */
    private opaque: (tx: number, ty: number) => boolean;

    constructor(private combat: CombatWorld) {
        // Дома (пропы) тоже перекрывают взгляд — сгустки не видят сквозь них.
        this.opaque = tileOpaque(combat.deps.map);
    }

    update(_world: World, dt: number): void {
        const playerPos = this.combat.deps.getPlayerPos();
        for (const [e, en] of this.combat.enemies) {
            const world = this.combat.world;
            if (en.brain.dead) {
                en.corpseTimer -= dt;
                if (en.corpseTimer <= 0) {
                    world.destroyEntity(e);
                    this.combat.enemies.delete(e);
                    this.combat.deps.registry?.remove(`enemy:${e}`);
                }
                continue;
            }

            // LOS с кэшем: пересчёт раз в LOS_INTERVAL.
            en.losTimer -= dt;
            if (en.losTimer <= 0) {
                en.los = hasLineOfSight(en.pos, playerPos, this.opaque);
                en.losTimer = LOS_INTERVAL;
            }

            const dx = playerPos.x - en.pos.x;
            const dy = playerPos.y - en.pos.y;
            const dist = worldDist(en.pos, playerPos) || 1e-4;
            const noise = this.combat.noiseFor(en);
            const intent: EnemyIntent = en.brain.update(dt, {
                dist,
                dirToPlayer: worldNorm(dx, dy),
                pos: en.pos,
                canSee: en.los,
                noise,
                patrolPoints: en.patrolPoints,
                home: en.home,
                hpFraction: en.hp / en.kind.hp
            });

            // Позиция до шага и толкучки: если сепарация втолкнет тело в стену
            // (separateCircles стены не проверяет), откатываемся сюда.
            const beforeCrowd = { ...en.pos };

            switch (intent.type) {
                case 'move':
                    stepWithCollision(
                        this.combat.deps.map,
                        en.pos,
                        intent.dir,
                        en.kind.speed * (intent.run ? RUN_MULT : 1),
                        en.kind.radius,
                        dt
                    );
                    this.combat.deps.registry?.move(`enemy:${e}`, en.pos);
                    break;
                case 'strike':
                    // Удар доходит, если герой в радиусе тела + замах
                    if (dist <= en.kind.attackRange + 0.25) {
                        this.combat.deps.damagePlayer(en.kind.attackDamage, en.pos);
                    }
                    break;
                case 'shoot':
                    this.combat.spawnProjectile(en.pos, intent.dir, en.kind.attackDamage);
                    // Плевок слышно (звук — подпиской на combat:spit), но спящих не будит (< 0.7)
                    this.combat.deps.events.emit('combat:spit', { pos: { ...en.pos } });
                    this.combat.noise(en.pos, 0.6);
                    break;
                default:
                    break;
            }

            // Тела: каждый враг выталкивает себя из героя, NPC и других врагов
            // (сепарация после шага — толкучка не отменяет намерение ИИ).
            separateCircles(en.pos, en.kind.radius, playerPos, PLAYER_COMBAT.radius);
            const reg = this.combat.deps.registry;
            if (reg) {
                for (const hit of reg.near(en.pos, en.kind.radius + BODY_PROBE)) {
                    if (hit.id === `enemy:${e}` || (hit.kind !== 'enemy' && hit.kind !== 'npc')) continue;
                    separateCircles(en.pos, en.kind.radius, hit.pos, hit.radius);
                }
            }
            // Стена сильнее толкучки: втолкнули в непроходимый тайл — откат.
            if (!circleFits(this.combat.deps.map, en.pos, en.kind.radius)) {
                en.pos.x = beforeCrowd.x;
                en.pos.y = beforeCrowd.y;
            }
        }
    }
}

/** Снаряды: полёт, попадание в героя, срок жизни. */
class ProjectileSystem implements System {
    constructor(private combat: CombatWorld) {}

    update(_world: World, dt: number): void {
        const playerPos = this.combat.deps.getPlayerPos();
        for (const [e, pr] of this.combat.projectiles) {
            pr.life -= dt;
            pr.pos.x += pr.vel.x * dt;
            pr.pos.y += pr.vel.y * dt;

            // Столкновение с героем
            if (inCircleW(playerPos, 0.2, pr.pos)) {
                this.combat.deps.damagePlayer(pr.damage, pr.pos);
                this.combat.killProjectile(e);
                continue;
            }
            // Вне карты, срок истёк или плевок в стену/дом (вода прозрачна —
            // летит над прудом; правило игры, не движка).
            const tile = worldToTile(pr.pos.x, pr.pos.y, this.combat.deps.map.data.width, this.combat.deps.map.data.height);
            if (pr.life <= 0 || !tile || this.combat.opaque(tile.x, tile.y)) {
                this.combat.killProjectile(e);
            }
        }
    }
}

// ---------- фасад ----------

export class CombatWorld {
    readonly world = new World();
    readonly enemies = new Map<Entity, EnemyComp>();
    readonly projectiles = new Map<Entity, ProjectileComp>();
    /** Последний шум мира (шаги, удары, плевки); живёт NOISE_WINDOW секунд. */
    private lastNoise: { origin: Vec2; level: number; age: number } | null = null;
    /** Переиспользуемый view шума для noiseFor — без копии origin на врага/тик. */
    private readonly noiseView: { origin: Vec2; level: number } = { origin: { x: 0, y: 0 }, level: 0 };
    /** Непрозрачность тайлов (стены/дома) — для конуса удара и снарядов. */
    readonly opaque: (tx: number, ty: number) => boolean;

    constructor(
        readonly deps: CombatDeps,
        private onEnemyKilled?: (kind: EnemyKindDef) => void
    ) {
        this.opaque = tileOpaque(deps.map);
        this.world.addSystem(new EnemySystem(this));
        this.world.addSystem(new ProjectileSystem(this));
    }

    /**
     * Заспавнить сгусток. Без patrol спит в пепле (dormant), пока не разбудит
     * гулкий звук; с патрулём сразу бродит по маршруту.
     */
    spawnEnemy(kindId: EnemyKindId, pos: Vec2, opts?: { patrol?: EnemyPatrolDef }): Entity {
        const kind = withDefaults(ENEMY_KINDS[kindId]);
        const e = this.world.createEntity();
        this.enemies.set(e, {
            kind,
            brain: new EnemyBrain(
                kind,
                opts?.patrol
                    ? { points: opts.patrol.points.map((p) => tileToWorld(p.x, p.y)), pauseSec: opts.patrol.pauseSec }
                    : undefined
            ),
            pos: { ...pos },
            hp: kind.hp,
            home: { ...pos },
            patrolPoints: opts?.patrol ? opts.patrol.points.map((p) => tileToWorld(p.x, p.y)) : [],
            corpseTimer: 0.5,
            los: false,
            losTimer: 0
        });
        // В реестре сцены — та же ссылка pos: move() после шага переиндексирует.
        this.deps.registry?.add({
            id: `enemy:${e}`,
            kind: 'enemy',
            pos: this.enemies.get(e)!.pos,
            radius: kind.radius,
            ref: e
        });
        return e;
    }

    spawnProjectile(from: Vec2, dir: Vec2, damage: number): Entity {
        const e = this.world.createEntity();
        this.projectiles.set(e, {
            pos: { x: from.x + dir.x * 0.3, y: from.y + dir.y * 0.3 },
            vel: { x: dir.x * 2.2, y: dir.y * 2.2 },
            damage,
            life: 1.5
        });
        return e;
    }

    killProjectile(e: Entity): void {
        if (!this.world.isAlive(e)) return;
        this.world.destroyEntity(e);
        this.projectiles.delete(e);
    }

    /**
     * Короткий удар конусом. Возвращает число поражённых сгустков.
     * Побочный эффект: гулкий звон (уровень 1.0) будит спящих в wakeRadius.
     */
    playerConeAttack(from: Vec2, dir: Vec2): number {
        this.noise(from, 1.0);
        this.deps.events.emit('combat:attack', { mode: 'cone', origin: { ...from } });

        let hits = 0;
        for (const [e, en] of this.enemies) {
            if (en.brain.dead) continue;
            if (!inConeW(from, dir, PLAYER_COMBAT.coneRange, PLAYER_COMBAT.coneHalfAngle, en.pos)) continue;
            // Звон не огибает дома: цель должна быть в прямой видимости.
            if (!hasLineOfSight(from, en.pos, this.opaque)) continue;
            this.damageEnemy(e, en, PLAYER_COMBAT.coneDamage, from);
            hits++;
        }
        return hits;
    }

    /** Низкий резонанс: усыпляет сгустков в радиусе. Возвращает число усыпленных. */
    resonancePulse(from: Vec2): number {
        this.deps.events.emit('combat:attack', { mode: 'resonance', origin: { ...from } });

        let slept = 0;
        for (const [, en] of this.enemies) {
            if (en.brain.dead) continue;
            if (inCircleW(from, PLAYER_COMBAT.resonanceRange, en.pos)) {
                en.brain.putToSleep(PLAYER_COMBAT.resonanceSleep);
                slept++;
            }
        }
        return slept;
    }

    /**
     * Шум в точке мира. level 0.35 — слышат бодрые в hearRadius (патруль
     * идёт проверять); 0.7+ — гулкий удар, будит и спящих в wakeRadius;
     * 0.6 — плевок/смерть сгустка (слышат, но не просыпаются).
     */
    noise(pos: Vec2, level: number): void {
        this.lastNoise = { origin: { ...pos }, level, age: 0 };
        if (level >= NOISE_WAKE_LEVEL) {
            for (const [, en] of this.enemies) {
                if (inCircleW(pos, PLAYER_COMBAT.wakeRadius, en.pos) && en.brain.asleep) {
                    en.brain.hearLoud();
                    this.deps.events.emit('combat:awake', { kind: en.kind.id, pos: { ...en.pos } });
                }
            }
        }
    }

    /** Шум, слышимый конкретному врагу (фильтр по уровню, радиусу слуха и окну). */
    noiseFor(en: EnemyComp): { origin: Vec2; level: number } | null {
        const n = this.lastNoise;
        if (!n || n.age > NOISE_WINDOW || n.level < NOISE_HEAR_LEVEL) return null;
        if (en.brain.asleep) return null; // спящего будит только noise() от 0.7
        if (worldDist(en.pos, n.origin) > (en.kind.hearRadius ?? 5)) return null;
        // Общий view (потребитель копирует origin перед хранением — см. EnemyBrain).
        this.noiseView.origin = n.origin;
        this.noiseView.level = n.level;
        return this.noiseView;
    }

    /** Урон сгустку; смерть — дроп, событие и приглушённый шум. */
    damageEnemy(e: Entity, en: EnemyComp, damage: number, from: Vec2): void {
        en.hp -= damage;
        en.brain.hurt();
        this.deps.events.emit('combat:hurt', { entity: e, died: en.hp <= 0 });
        if (en.hp <= 0) {
            if (en.brain.die()) {
                this.noise(en.pos, 0.6);
                this.deps.events.emit('combat:kill', { kind: en.kind.id, tile: { ...en.pos } });
                this.onEnemyKilled?.(en.kind);
                // Отброс трупа от героя — лёгкий визуальный эффект делает CombatViews.
                en.pos.x += Math.sign(en.pos.x - from.x) * 0.0625;
                en.pos.y += Math.sign(en.pos.y - from.y) * 0.0625;
            }
        }
    }

    /** Инварианты боевого мира: легальность состояний и позиций врагов. */
    agentInvariants(): Invariant[] {
        const where = 'systems/combat';
        const { width, height } = this.deps.map.data;
        const out: Invariant[] = [];
        for (const [, en] of this.enemies) {
            if (!ENEMY_STATES.includes(en.brain.state as (typeof ENEMY_STATES)[number])) {
                out.push({
                    id: 'enemy-state-legal',
                    severity: 'error',
                    message: `${en.kind.id}: неизвестное состояние "${en.brain.state}"`,
                    where
                });
            }
            const tx = Math.floor(en.pos.x);
            const ty = Math.floor(en.pos.y);
            if (tx < 0 || ty < 0 || tx >= width || ty >= height) {
                out.push({
                    id: 'enemy-in-bounds',
                    severity: 'error',
                    message: `${en.kind.id} вне карты (${en.pos.x.toFixed(2)},${en.pos.y.toFixed(2)})`,
                    where
                });
            }
        }
        return out;
    }

    update(dt: number): void {
        if (this.lastNoise) {
            this.lastNoise.age += dt;
            if (this.lastNoise.age > NOISE_WINDOW) this.lastNoise = null;
        }
        this.world.update(dt);
    }
}