// AccessoryManager — крепление GLB-аксессуаров (шляпы, инструменты,
// причёски, лица) к костям R15-скелета персонажа.
//
// Подфаза 3.2 RUBLOX_ACCESSORY_ATTACHMENT_PLAN.md (2026-05-26).
//
// Принцип:
//   1. Грузим GLB аксессуара через SceneLoader.LoadAssetContainerAsync.
//   2. Резолвим кость (Head/RightHand/...) через R15Skeleton.resolveBone().
//   3. Создаём якорь — TransformNode, привязанный к кости через attachToBone().
//   4. Локальный transform якоря = offset/rotation/scale из item.attachment.
//   5. Меш аксессуара парентим к якорю — он автоматом движется с костью.
//
// Жизненный цикл управляется по slot (hat/tool/hair/face/...):
//   attach(item)        — при повторном attach в тот же слот сначала detach
//   detachSlot(slot)    — убирает аксессуар из конкретного слота
//   detachAll()         — при смене body-скина / выходе из игры
//   updateAttachment(slot, transform) — live-preview во время калибровки
//
// ВАЖНО: skinMesh для attachToBone — это _modelRoot тела, который
// PlayerController создаёт в _loadPlayerModel() (строка 629).
// Если bone не найден — fallback: parent = modelRoot + дефолтная позиция.

import {
    Vector3, Quaternion, SceneLoader, TransformNode,
} from '@babylonjs/core';

// Дефолтные позиции аксессуара относительно кости (если slot_bone не задан
// или bone не нашёлся). По типу скина (item.type) — те же что в SLOT_TO_BONE_DEFAULTS
// в Designer.py. Используется только как fallback — обычно дизайнер
// настраивает offset через калибровку.
const TYPE_TO_DEFAULT_BONE = {
    hat:  'Head',
    hair: 'Head',
    face: 'Head',
    tool: 'RightHand',
    tool_left: 'LeftHand',
};

export class AccessoryManager {
    /**
     * @param scene — Babylon Scene
     * @param r15Skeleton — R15Skeleton instance (из PlayerController._r15Skeleton)
     * @param bodyRoot — TransformNode тела (PlayerController._modelRoot),
     *                   нужен для attachToBone() и для fallback-parent.
     */
    constructor(scene, r15Skeleton, bodyRoot) {
        this.scene = scene;
        this.r15Skeleton = r15Skeleton;
        this.bodyRoot = bodyRoot;
        // slot → { item, anchor, container, rootNodes[], isFallback }
        this._slots = {};
    }

    /**
     * Прицепить аксессуар к персонажу. Если в этом слоте уже что-то надето —
     * сначала снимаем. Возвращает Promise<handle> с методом handle.dispose().
     */
    async attach(item) {
        if (!item || !item.path) {
            throw new Error('AccessoryManager.attach: item.path required');
        }
        if (item.is_body_skin) {
            // Body-skins (shirt/pants) грузятся через body-replace, не сюда.
            // Подфаза 3.6 — отдельный поток.
            // eslint-disable-next-line no-console
            console.warn('[AccessoryManager] body_skin item passed, ignoring',
                         item.id);
            return null;
        }
        const slot = item.type;
        if (!slot) {
            throw new Error('AccessoryManager.attach: item.type required');
        }
        // Уже надетый аксессуар в этом слоте — снимаем
        if (this._slots[slot]) {
            this.detachSlot(slot);
        }

        // Грузим GLB-контейнер
        const url = item.path;
        const lastSlash = url.lastIndexOf('/');
        const rootUrl = url.substring(0, lastSlash + 1);
        const filename = url.substring(lastSlash + 1);
        let container;
        try {
            container = await SceneLoader.LoadAssetContainerAsync(
                rootUrl, filename, this.scene
            );
        } catch (e) {
            // eslint-disable-next-line no-console
            console.error('[AccessoryManager] failed to load', url, e);
            throw e;
        }

        // Резолвим bone
        const attachment = item.attachment || {};
        const boneName = attachment.slot_bone || TYPE_TO_DEFAULT_BONE[slot] || null;
        const bone = boneName && this.r15Skeleton
            ? this.r15Skeleton.resolveBone(boneName) : null;

        // Создаём якорь — это TransformNode, к которому прикрутим меш.
        const anchor = new TransformNode(`acc_anchor_${slot}_${item.id}`, this.scene);

        let isFallback = false;
        if (bone) {
            // attachToBone(bone, affectedMesh): меш якоря (а через parent —
            // все его дети) будет двигаться по местоположению кости каждый
            // кадр. affectedMesh — это «носитель скелета», ему мы говорим
            // что якорь привязан к одной из его костей.
            // Сам аксессуар (anchor child) рендерится независимо, но
            // его world-transform = bone world-transform * anchor local.
            try {
                const skinHostMesh = this._findSkinHostMesh();
                anchor.attachToBone(bone, skinHostMesh);
            } catch (e) {
                // eslint-disable-next-line no-console
                console.warn('[AccessoryManager] attachToBone failed, fallback to bodyRoot',
                             e);
                anchor.parent = this.bodyRoot;
                isFallback = true;
            }
        } else {
            // Bone не нашёлся — fallback на bodyRoot. Без анимации не подвинется.
            // eslint-disable-next-line no-console
            console.warn(`[AccessoryManager] bone "${boneName}" not found, fallback`);
            anchor.parent = this.bodyRoot;
            isFallback = true;
        }

        // Применяем offset/rotation/scale
        this._applyTransformToAnchor(anchor, attachment);

        // Инстанцируем модель аксессуара под якорь
        const inst = container.instantiateModelsToScene(
            (name) => `acc_${slot}_${name}`,
            /*cloneAnimations*/ false,
            { doNotInstantiate: false },
        );
        const rootNodes = [];
        for (const r of inst.rootNodes) {
            r.parent = anchor;
            rootNodes.push(r);
        }

        const handle = {
            slot,
            item,
            anchor,
            container,
            rootNodes,
            isFallback,
            dispose: () => this.detachSlot(slot),
        };
        this._slots[slot] = handle;
        return handle;
    }

    /** Снять аксессуар из конкретного слота. */
    detachSlot(slot) {
        const h = this._slots[slot];
        if (!h) return;
        delete this._slots[slot];
        try {
            // Чистим инстансы
            for (const r of h.rootNodes) {
                try { r.dispose(false, true); } catch (e) {}
            }
            try { h.anchor.dispose(false, true); } catch (e) {}
            // Контейнер тоже освобождаем (геометрии/материалы/текстуры)
            try { h.container.dispose(); } catch (e) {}
        } catch (e) {
            // eslint-disable-next-line no-console
            console.warn('[AccessoryManager] detach cleanup error:', e);
        }
    }

    /** Снять все аксессуары (при смене body-скина или destroy). */
    detachAll() {
        for (const slot of Object.keys(this._slots)) {
            this.detachSlot(slot);
        }
    }

    /**
     * Обновить attachment-параметры на уже надетом аксессуаре. Используется
     * на странице калибровки: дизайнер двигает гизмо → мы вызываем сюда
     * → меш двигается без полной перезагрузки GLB. transform — частичный
     * объект {slot_bone?, offset?, rotation?, scale?}.
     */
    updateAttachment(slot, transform) {
        const h = this._slots[slot];
        if (!h) return false;
        // Если slot_bone меняется — нужен полный re-attach, потому что
        // attachToBone() прибит к конкретной кости. Делаем перецепку:
        const oldBoneName = (h.item.attachment && h.item.attachment.slot_bone)
            || TYPE_TO_DEFAULT_BONE[slot];
        const newBoneName = transform.slot_bone !== undefined
            ? transform.slot_bone : oldBoneName;
        if (newBoneName !== oldBoneName) {
            // Сливаем новые поля в item и переподключаем
            const newItem = {
                ...h.item,
                attachment: {
                    ...h.item.attachment,
                    ...transform,
                },
            };
            this.detachSlot(slot);
            return this.attach(newItem);  // Promise!
        }
        // Иначе — просто меняем локальный transform якоря
        this._applyTransformToAnchor(h.anchor, {
            ...h.item.attachment,
            ...transform,
        });
        // Обновляем item.attachment чтобы повторные updateAttachment работали правильно
        h.item.attachment = { ...h.item.attachment, ...transform };
        return true;
    }

    // ─── helpers ─────────────────────────────────────────────────────

    _applyTransformToAnchor(anchor, attachment) {
        const off = attachment.offset || [0, 0, 0];
        const rot = attachment.rotation || [0, 0, 0];
        const scale = attachment.scale != null ? attachment.scale : 1.0;
        anchor.position = new Vector3(off[0] || 0, off[1] || 0, off[2] || 0);
        // Babylon: rotation в радианах через rotationQuaternion (избегаем
        // gimbal-lock и проблем с порядком осей). YawPitchRoll(y, x, z).
        anchor.rotationQuaternion = Quaternion.RotationYawPitchRoll(
            rot[1] || 0, rot[0] || 0, rot[2] || 0
        );
        const s = scale || 1.0;
        anchor.scaling = new Vector3(s, s, s);
    }

    /**
     * Найти скиннингованный mesh тела (нужен Babylon-у в attachToBone()
     * чтобы он знал из какого AssetContainer'а взять Bone-таблицу для
     * cached world-transform). Берём первый mesh из bodyRoot.getChildMeshes()
     * у которого есть skeleton.
     */
    _findSkinHostMesh() {
        if (!this.bodyRoot) return null;
        const meshes = this.bodyRoot.getChildMeshes
            ? this.bodyRoot.getChildMeshes() : [];
        for (const m of meshes) {
            if (m && m.skeleton) return m;
        }
        // Fallback: первый mesh — attachToBone примет и без skeleton,
        // но bone-привязка может стать менее точной.
        return meshes[0] || null;
    }
}
