import { EntityWorld, type Entity } from '../core/ecs';
import { hasLineOfSight } from './los';
import { horDist, horNorm, inCircle, inCone, type Hor } from './shapes';
import { ENEMY_KINDS, withDefaults, type EnemyKindDef, type EnemyKindId, type EnemyPatrolDef } from './enemies';
import { EnemyBrain, ENEMY_STATES, NOISE_HEAR_LEVEL, NOISE_WAKE_LEVEL, type EnemyIntent } from './brain';
import { PLAYER_COMBAT } from './stats';

/**
 * Боевой мир (перенос v1, адаптация к вокселям): сущности (враги, снаряды)
 * в движковом ECS, поведение — в системах; вьюхи держатся отдельно.
 *
 * Отличия от v1: бой живёт в горизонтальной плоскости x/z; вместо
 * тайловой карты — предикаты колонок мира: `solid` (блокирует движение)
 * и `opaque` (блокирует взгляд и снаряды); вместо EventBus — хуки
 * `hooks` (вьюхи/звук подписываются игрой).
 */

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

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

/** Хуки боевого мира: реакции вьюх/звука (в v1 — события EventBus). */
export interface CombatHooks {
    /** Сгусток получил урон (died — добит). */
    onEnemyHurt?(entity: Entity, died: boolean): void;
    /** Сгусток умер (дроп пыли, вспышка). */
    onEnemyKilled?(kindId: EnemyKindId, pos: Hor): void;
    /** Спящий сгусток разбужен гулким звуком. */
    onEnemyAwake?(kindId: EnemyKindId, pos: Hor): void;
    /** Атака героя (конус или резонанс) — вьюха сектора/волны. */
    onAttack?(mode: 'cone' | 'resonance', origin: Hor): void;
    /** Плевок сгустка (звук; спящих не будит). */
    onSpit?(pos: Hor): void;
}

export interface CombatDeps {
    /** Позиция героя (ноги), юниты. */
    getPlayerPos: () => Hor;
    /** Герой получил урон (сцена отбросит/вспыхнет/проиграет звук). */
    damagePlayer: (damage: number, from: Hor) => void;
    /** Колонка (cx, cz) блокирует движение (стены, дом). */
    solid: (cx: number, cz: number) => boolean;
    /** Колонка блокирует взгляд и снаряды (высокие объекты). */
    opaque: (cx: number, cz: number) => boolean;
    /** Хуки реакций (опция). */
    hooks?: CombatHooks;
    /** Границы мира для инварианта «враг в пределах» (опция). */
    bounds?: { width: number; height: number };
}

/** Окно слышимости шума: событие живёт столько секунд для бодрых ушей. */
const NOISE_WINDOW = 0.5;
/** Пересчёт LOS раз в 3 тика (60 Гц) — видимость меняется медленнее. */
const LOS_INTERVAL = 0.05;
/** Бег (отступление) быстрее шага. */
const RUN_MULT = 1.3;
/** Зазор от грани колонки при зажиме круга. */
const EPS = 0.001;
/** Радиус попадания снаряда в героя, юнитов. */
const PLAYER_HIT_RADIUS = 0.2;
/** Скорость плевка и срок жизни, юнитов/с и сек. */
const PROJECTILE_SPEED = 2.2;
const PROJECTILE_LIFE = 1.5;

// ---------- движение круга по колонкам ----------

/** Пересекает ли круг хоть одну solid-колонку (граница-касание разрешена). */
export function circleHitsSolid(x: number, z: number, r: number, solid: (cx: number, cz: number) => boolean): boolean {
    const minX = Math.floor(x - r);
    const maxX = Math.floor(x + r);
    const minZ = Math.floor(z - r);
    const maxZ = Math.floor(z + r);
    for (let cx = minX; cx <= maxX; cx++) {
        for (let cz = minZ; cz <= maxZ; cz++) {
            if (!solid(cx, cz)) continue;
            // Ближайшая к центру круга точка ячейки
            const px = Math.max(cx, Math.min(x, cx + 1));
            const pz = Math.max(cz, Math.min(z, cz + 1));
            const dx = x - px;
            const dz = z - pz;
            if (dx * dx + dz * dz < r * r) return true;
        }
    }
    return false;
}

/**
 * Движение круга с ось-раздельным скольжением вдоль стен (X → Z): застряв
 * в solid-колонку, зажимаем к её грани — по аналогии с физикой тел v2.
 */
export function moveCircleHor(pos: Hor, delta: Hor, r: number, solid: (cx: number, cz: number) => boolean): void {
    if (delta.x !== 0) {
        pos.x += delta.x;
        if (circleHitsSolid(pos.x, pos.z, r, solid)) {
            pos.x = delta.x > 0 ? Math.floor(pos.x + r) - r - EPS : Math.floor(pos.x - r) + 1 + r + EPS;
        }
    }
    if (delta.z !== 0) {
        pos.z += delta.z;
        if (circleHitsSolid(pos.x, pos.z, r, solid)) {
            pos.z = delta.z > 0 ? Math.floor(pos.z + r) - r - EPS : Math.floor(pos.z - r) + 1 + r + EPS;
        }
    }
}

/** Вытолкнуть self из пересечения с чужим кругом (двигается только self). */
function separate(self: Hor, selfR: number, other: Hor, otherR: number): void {
    const dx = self.x - other.x;
    const dz = self.z - other.z;
    const d = Math.hypot(dx, dz) || 1e-4;
    const overlap = selfR + otherR - d;
    if (overlap > 0) {
        self.x += (dx / d) * overlap;
        self.z += (dz / d) * overlap;
    }
}

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

/** ИИ и движение сгустков. */
class EnemySystem {
    constructor(private combat: CombatWorld) {}

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

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

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

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

            switch (intent.type) {
                case 'move': {
                    const speed = en.kind.speed * (intent.run ? RUN_MULT : 1);
                    moveCircleHor(en.pos, { x: intent.dir.x * speed * dt, z: intent.dir.z * speed * dt }, en.kind.radius, combat.deps.solid);
                    break;
                }
                case 'strike':
                    // Удар доходит, если герой в радиусе тела + замах
                    if (dist <= en.kind.attackRange + 0.25) {
                        combat.deps.damagePlayer(en.kind.attackDamage, { ...en.pos });
                    }
                    break;
                case 'shoot':
                    combat.spawnProjectile(en.pos, intent.dir, en.kind.attackDamage);
                    combat.deps.hooks?.onSpit?.({ ...en.pos });
                    combat.noise(en.pos, 0.6); // слышно, но спящих не будит
                    break;
                default:
                    break;
            }

            // Тела: каждый враг выталкивает себя из героя и других врагов
            // (сепарация после шага — толкучка не отменяет намерение ИИ).
            separate(en.pos, en.kind.radius, playerPos, PLAYER_COMBAT.radius);
            for (const [o, other] of combat.enemies) {
                if (o === e || other.brain.dead) continue;
                separate(en.pos, en.kind.radius, other.pos, other.kind.radius);
            }
            // Стена сильнее толкучки: втолкнули в solid-колонку — откат.
            if (circleHitsSolid(en.pos.x, en.pos.z, en.kind.radius, combat.deps.solid)) {
                en.pos.x = beforeCrowd.x;
                en.pos.z = beforeCrowd.z;
            }
        }
    }
}

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

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

            // Столкновение с героем
            if (inCircle(playerPos, PLAYER_HIT_RADIUS, pr.pos)) {
                combat.deps.damagePlayer(pr.damage, { ...pr.pos });
                combat.killProjectile(e);
                continue;
            }
            // Срок истёк или плевок в стену/дом (правила прозрачности — игра).
            if (pr.life <= 0 || combat.deps.opaque(Math.floor(pr.pos.x), Math.floor(pr.pos.z))) {
                combat.killProjectile(e);
            }
        }
    }
}

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

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

    constructor(readonly deps: CombatDeps) {
        this.world.addSystem(new EnemySystem(this));
        this.world.addSystem(new ProjectileSystem(this));
    }

    /**
     * Заспавнить сгусток. Без patrol спит в пепле (dormant), пока не разбудит
     * гулкий звук; с патрулём сразу бродит по маршруту.
     */
    spawnEnemy(kindId: EnemyKindId, pos: Hor, opts?: { patrol?: EnemyPatrolDef }): Entity {
        const kind = withDefaults(ENEMY_KINDS[kindId]);
        const points = opts?.patrol ? opts.patrol.points.map((p) => ({ ...p })) : undefined;
        const e = this.world.createEntity();
        this.enemies.set(e, {
            kind,
            brain: new EnemyBrain(kind, points ? { points, pauseSec: opts!.patrol!.pauseSec } : undefined),
            pos: { ...pos },
            hp: kind.hp,
            home: { ...pos },
            patrolPoints: points ?? [],
            corpseTimer: 0.5,
            los: false,
            losTimer: 0,
        });
        return e;
    }

    spawnProjectile(from: Hor, dir: Hor, damage: number): Entity {
        const e = this.world.createEntity();
        this.projectiles.set(e, {
            pos: { x: from.x + dir.x * 0.3, z: from.z + dir.z * 0.3 },
            vel: { x: dir.x * PROJECTILE_SPEED, z: dir.z * PROJECTILE_SPEED },
            damage,
            life: PROJECTILE_LIFE,
        });
        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: Hor, dir: Hor): number {
        this.noise(from, 1.0);
        this.deps.hooks?.onAttack?.('cone', { ...from });

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

    /** Низкий резонанс: усыпляет сгустков в радиусе. Возвращает число усыпленных. */
    resonancePulse(from: Hor): number {
        this.deps.hooks?.onAttack?.('resonance', { ...from });

        let slept = 0;
        for (const [, en] of this.enemies) {
            if (en.brain.dead) continue;
            if (inCircle(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: Hor, level: number): void {
        this.lastNoise = { origin: { ...pos }, level, age: 0 };
        if (level >= NOISE_WAKE_LEVEL) {
            for (const [, en] of this.enemies) {
                if (inCircle(pos, PLAYER_COMBAT.wakeRadius, en.pos) && en.brain.asleep) {
                    en.brain.hearLoud();
                    this.deps.hooks?.onEnemyAwake?.(en.kind.id, { ...en.pos });
                }
            }
        }
    }

    /** Шум, слышимый конкретному врагу (фильтр по уровню, радиусу слуха и окну). */
    noiseFor(en: EnemyComp): { origin: Hor; 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 (horDist(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: Hor): void {
        en.hp -= damage;
        en.brain.hurt();
        this.deps.hooks?.onEnemyHurt?.(e, en.hp <= 0);
        if (en.hp <= 0) {
            if (en.brain.die()) {
                this.noise(en.pos, 0.6);
                this.deps.hooks?.onEnemyKilled?.(en.kind.id, { ...en.pos });
                // Отброс трупа от героя — лёгкий визуальный эффект на игре.
                en.pos.x += Math.sign(en.pos.x - from.x) * 0.0625;
                en.pos.z += Math.sign(en.pos.z - from.z) * 0.0625;
            }
        }
    }

    /** Инварианты боевого мира: легальность состояний и позиций врагов. */
    agentInvariants(): { id: string; severity: 'error'; message: string }[] {
        const out: { id: string; severity: 'error'; message: string }[] = [];
        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}"` });
            }
            if (this.deps.bounds) {
                const { width, height } = this.deps.bounds;
                const tx = Math.floor(en.pos.x);
                const tz = Math.floor(en.pos.z);
                if (tx < 0 || tz < 0 || tx >= width || tz >= height) {
                    out.push({ id: 'enemy-in-bounds', severity: 'error', message: `${en.kind.id} вне мира (${en.pos.x.toFixed(2)},${en.pos.z.toFixed(2)})` });
                }
            }
        }
        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);
    }
}