import { NpcEntityModel } from "../models/NpcEntityModel.js";
import { FactionEntityModel } from "../models/FactionEntityModel.js";
import { ShipEntityModel } from "../models/ShipEntityModel.js";
import { LocationEntityModel } from "../models/LocationEntityModel.js";
import { NpcEntityNormalizer } from "./NpcEntityNormalizer.js";
import { FactionEntityNormalizer } from "./FactionEntityNormalizer.js";
import { ShipEntityNormalizer } from "./ShipEntityNormalizer.js";
import { LocationEntityNormalizer } from "./LocationEntityNormalizer.js";
import { PersonalityTraitRegistry } from "./PersonalityTraitRegistry.js";

export class CampaignEntityIndex {
    static #npcById = new Map();
    static #npcIdByJournalEntryId = new Map();
    static #factionById = new Map();
    static #factionIdByJournalEntryId = new Map();
    static #shipById = new Map();
    static #shipIdByJournalEntryId = new Map();
    static #locationById = new Map();
    static #locationIdByJournalEntryId = new Map();
    static #rebuilt = false;

    static async initialize() {
        await PersonalityTraitRegistry.load();
        this.rebuild();
        this.#registerHooks();
    }

    static rebuild() {
        this.#npcById.clear();
        this.#npcIdByJournalEntryId.clear();
        this.#factionById.clear();
        this.#factionIdByJournalEntryId.clear();
        this.#shipById.clear();
        this.#shipIdByJournalEntryId.clear();
        this.#locationById.clear();
        this.#locationIdByJournalEntryId.clear();
        for (const entry of game.journal?.contents || []) {
            this.upsert(entry);
        }
        this.#rebuilt = true;
        return this.getNpcs();
    }

    static ensureBuilt() {
        if (!this.#rebuilt) this.rebuild();
    }

    static upsert(document) {
        this.#removeByJournalEntryId(document?.id);
        const raw = NpcEntityModel.fromDocument(document);
        if (raw?.id) return this.#upsertNpc(raw);
        const factionRaw = FactionEntityModel.fromDocument(document);
        if (factionRaw?.id) return this.#upsertFaction(factionRaw);
        const shipRaw = ShipEntityModel.fromDocument(document);
        if (shipRaw?.id) return this.#upsertShip(shipRaw);
        const locationRaw = LocationEntityModel.fromDocument(document);
        if (locationRaw?.id) return this.#upsertLocation(locationRaw);
        return null;
    }

    static remove(id) {
        const entityId = String(id || "").trim();
        if (!entityId) return;
        const entity = this.#npcById.get(entityId);
        if (entity?.journalEntryId) this.#npcIdByJournalEntryId.delete(entity.journalEntryId);
        this.#npcById.delete(entityId);
        const faction = this.#factionById.get(entityId);
        if (faction?.journalEntryId) this.#factionIdByJournalEntryId.delete(faction.journalEntryId);
        this.#factionById.delete(entityId);
        const ship = this.#shipById.get(entityId);
        if (ship?.journalEntryId) this.#shipIdByJournalEntryId.delete(ship.journalEntryId);
        this.#shipById.delete(entityId);
        const location = this.#locationById.get(entityId);
        if (location?.journalEntryId) this.#locationIdByJournalEntryId.delete(location.journalEntryId);
        this.#locationById.delete(entityId);
    }

    static getNpc(idOrUuid) {
        this.ensureBuilt();
        const value = String(idOrUuid || "").trim();
        if (!value) return null;
        if (this.#npcById.has(value)) return { ...this.#npcById.get(value) };
        return [...this.#npcById.values()].find(entity =>
            entity.uuid === value
            || entity.journalEntryId === value
            || `JournalEntry.${entity.journalEntryId}` === value
        ) || null;
    }

    static getNpcs({ search = "" } = {}) {
        this.ensureBuilt();
        const query = String(search || "").trim().toLocaleLowerCase();
        const rows = [...this.#npcById.values()]
            .sort((a, b) => NpcEntityModel.getName(a).localeCompare(NpcEntityModel.getName(b)));
        if (!query) return rows.map(entity => ({ ...entity }));
        return rows
            .filter(entity => NpcEntityModel.getSearchText(entity).includes(query))
            .map(entity => ({ ...entity }));
    }

    static getFaction(idOrUuid) {
        this.ensureBuilt();
        const value = String(idOrUuid || "").trim();
        if (!value) return null;
        if (this.#factionById.has(value)) return { ...this.#factionById.get(value) };
        return [...this.#factionById.values()].find(entity =>
            entity.uuid === value
            || entity.journalEntryId === value
            || `JournalEntry.${entity.journalEntryId}` === value
        ) || null;
    }

    static getFactions({ search = "" } = {}) {
        this.ensureBuilt();
        const query = String(search || "").trim().toLocaleLowerCase();
        const rows = [...this.#factionById.values()]
            .sort((a, b) => FactionEntityModel.getName(a).localeCompare(FactionEntityModel.getName(b)));
        if (!query) return rows.map(entity => ({ ...entity }));
        return rows
            .filter(entity => FactionEntityModel.getSearchText(entity).includes(query))
            .map(entity => ({ ...entity }));
    }

    static getShip(idOrUuid) {
        this.ensureBuilt();
        const value = String(idOrUuid || "").trim();
        if (!value) return null;
        if (this.#shipById.has(value)) return { ...this.#shipById.get(value) };
        return [...this.#shipById.values()].find(entity =>
            entity.uuid === value || entity.journalEntryId === value || `JournalEntry.${entity.journalEntryId}` === value
        ) || null;
    }

    static getShips({ search = "" } = {}) {
        this.ensureBuilt();
        const query = String(search || "").trim().toLocaleLowerCase();
        const rows = [...this.#shipById.values()].sort((a, b) => ShipEntityModel.getName(a).localeCompare(ShipEntityModel.getName(b)));
        if (!query) return rows.map(entity => ({ ...entity }));
        return rows.filter(entity => ShipEntityModel.getSearchText(entity).includes(query)).map(entity => ({ ...entity }));
    }

    static getLocation(idOrUuid) {
        this.ensureBuilt();
        const value = String(idOrUuid || "").trim();
        if (!value) return null;
        if (this.#locationById.has(value)) return this.#cloneEntity(this.#locationById.get(value));
        const entityId = this.#locationIdByJournalEntryId.get(this.#journalEntryId(value));
        if (entityId) return this.#cloneEntity(this.#locationById.get(entityId));
        const entity = [...this.#locationById.values()].find(candidate => candidate.uuid === value);
        return this.#cloneEntity(entity);
    }

    static getLocations({ search = "" } = {}) {
        this.ensureBuilt();
        const query = String(search || "").trim().toLocaleLowerCase();
        const rows = [...this.#locationById.values()]
            .sort((a, b) => LocationEntityModel.getName(a).localeCompare(LocationEntityModel.getName(b)));
        if (!query) return rows.map(entity => this.#cloneEntity(entity));
        return rows
            .filter(entity => LocationEntityModel.getSearchText(entity).includes(query))
            .map(entity => this.#cloneEntity(entity));
    }

    static resolveLocationDocument(idOrUuid) {
        this.ensureBuilt();
        const entity = this.getLocation(idOrUuid);
        return entity?.journalEntryId ? game.journal?.get(entity.journalEntryId) || null : null;
    }

    static #upsertNpc(raw) {
        const entity = NpcEntityNormalizer.normalizeExisting(raw);
        const existing = this.#npcById.get(entity.id);
        if (existing?.journalEntryId) this.#npcIdByJournalEntryId.delete(existing.journalEntryId);
        this.#npcById.set(entity.id, entity);
        if (entity.journalEntryId) this.#npcIdByJournalEntryId.set(entity.journalEntryId, entity.id);
        return entity;
    }

    static #upsertFaction(raw) {
        const entity = FactionEntityNormalizer.normalizeExisting(raw);
        const existing = this.#factionById.get(entity.id);
        if (existing?.journalEntryId) this.#factionIdByJournalEntryId.delete(existing.journalEntryId);
        this.#factionById.set(entity.id, entity);
        if (entity.journalEntryId) this.#factionIdByJournalEntryId.set(entity.journalEntryId, entity.id);
        return entity;
    }

    static #upsertShip(raw) {
        const entity = ShipEntityNormalizer.normalizeExisting(raw);
        const existing = this.#shipById.get(entity.id);
        if (existing?.journalEntryId) this.#shipIdByJournalEntryId.delete(existing.journalEntryId);
        this.#shipById.set(entity.id, entity);
        if (entity.journalEntryId) this.#shipIdByJournalEntryId.set(entity.journalEntryId, entity.id);
        return entity;
    }

    static #upsertLocation(raw) {
        const normalized = LocationEntityNormalizer.normalizeExisting(raw);
        const entity = {
            ...normalized,
            uuid: String(raw.uuid || ""),
            journalEntryId: String(raw.journalEntryId || ""),
            journalEntryName: String(raw.journalEntryName || LocationEntityModel.getName(normalized))
        };
        const existing = this.#locationById.get(entity.id);
        if (existing?.journalEntryId) this.#locationIdByJournalEntryId.delete(existing.journalEntryId);
        this.#locationById.set(entity.id, entity);
        if (entity.journalEntryId) this.#locationIdByJournalEntryId.set(entity.journalEntryId, entity.id);
        return entity;
    }

    static #journalEntryId(value) {
        const match = String(value || "").match(/^JournalEntry\.([^.]+)$/);
        return match?.[1] || String(value || "");
    }

    static #removeByJournalEntryId(journalEntryId) {
        const documentId = String(journalEntryId || "").trim();
        if (!documentId) return;
        for (const reverseIndex of [
            this.#npcIdByJournalEntryId,
            this.#factionIdByJournalEntryId,
            this.#shipIdByJournalEntryId,
            this.#locationIdByJournalEntryId
        ]) {
            const entityId = reverseIndex.get(documentId);
            if (entityId) this.remove(entityId);
        }
    }

    static #registerHooks() {
        if (this._registered) return;
        this._registered = true;
        Hooks.on("createJournalEntry", document => {
            if (this.#isIndexedDocument(document)) {
                this.upsert(document);
                Hooks.callAll("augurNexusCampaignEntitiesChanged", { reason: "journalCreate", document });
            }
        });
        Hooks.on("updateJournalEntry", document => {
            const previousId = document?.id ? this.#npcIdByJournalEntryId.get(document.id) : "";
            const previousFactionId = document?.id ? this.#factionIdByJournalEntryId.get(document.id) : "";
            const previousShipId = document?.id ? this.#shipIdByJournalEntryId.get(document.id) : "";
            const previousLocationId = document?.id ? this.#locationIdByJournalEntryId.get(document.id) : "";
            if (this.#isIndexedDocument(document)) {
                this.upsert(document);
                Hooks.callAll("augurNexusCampaignEntitiesChanged", { reason: "journalUpdate", document });
            } else if (previousId) {
                this.remove(previousId);
                Hooks.callAll("augurNexusCampaignEntitiesChanged", { reason: "journalUpdateRemoved", document });
            } else if (previousFactionId) {
                this.remove(previousFactionId);
                Hooks.callAll("augurNexusCampaignEntitiesChanged", { reason: "journalUpdateRemoved", document });
            } else if (previousLocationId) {
                this.remove(previousLocationId);
                Hooks.callAll("augurNexusCampaignEntitiesChanged", { reason: "journalUpdateRemoved", document });
            } else if (previousShipId) {
                this.remove(previousShipId);
                Hooks.callAll("augurNexusCampaignEntitiesChanged", { reason: "journalUpdateRemoved", document });
            }
        });
        Hooks.on("deleteJournalEntry", (document, options = {}) => {
            const entity = NpcEntityModel.fromDocument(document)
                || FactionEntityModel.fromDocument(document)
                || ShipEntityModel.fromDocument(document)
                || LocationEntityModel.fromDocument(document);
            const entityId = entity?.id
                || (document?.id ? this.#npcIdByJournalEntryId.get(document.id) : "")
                || (document?.id ? this.#factionIdByJournalEntryId.get(document.id) : "")
                || (document?.id ? this.#shipIdByJournalEntryId.get(document.id) : "")
                || (document?.id ? this.#locationIdByJournalEntryId.get(document.id) : "");
            if (!entityId) return;
            this.remove(entityId);
            if (options?.["augur-nexus"]?.managedCascade) return;
            Hooks.callAll("augurNexusCampaignEntitiesChanged", { reason: "journalDelete", entity, document });
        });
    }

    static #cloneEntity(entity) {
        return entity ? foundry.utils.deepClone(entity) : null;
    }

    static #isIndexedDocument(document) {
        return NpcEntityModel.isNpcDocument(document)
            || FactionEntityModel.isFactionDocument(document)
            || ShipEntityModel.isShipDocument(document)
            || LocationEntityModel.isLocationDocument(document);
    }
}
