import SpellRepo, { SpellResult } from '../repositories/spell.repo.js';
import SpellTagRepo, { SpellTagResult } from '../repositories/spelltag.repo.js';
import BookRepo, { BookToolsTable } from '../repositories/book.repository.js';
import System from '../System.js';
import { getUserEncryptionKey } from '../keyManager.js';
import RemovedItem from './RemovedItem.js';

export interface SpellTagProps {
    id: string;
    name: string;
    color: string | null;
}

export interface SpellProps {
    id: string;
    name: string;
    description: string;
    appearance: string;
    tags: string[];
    powerLevel: string | null;
    components: string | null;
    limitations: string | null;
    notes: string | null;
    seriesSpellId: string | null;
}

export interface SpellListItem {
    id: string;
    name: string;
    description: string;
    tags: SpellTagProps[];
    seriesSpellId?: string | null;
}

export interface SpellListResponse {
    enabled: boolean;
    spells: SpellListItem[];
    tags: SpellTagProps[];
}

export interface SyncedSpell {
    id: string;
    name: string;
    lastUpdate: number;
}

export interface SyncedSpellTag {
    id: string;
    name: string;
    lastUpdate: number;
}

export default class Spell {
    /**
     * Retrieves all spell tags for a specific book.
     * @param userId - The unique identifier of the user
     * @param bookId - The unique identifier of the book
     * @param lang - The language for error messages ('fr' or 'en')
     * @returns An array of spell tag props
     */
    static getSpellTags(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): SpellTagProps[] {
        const userKey: string = getUserEncryptionKey(userId);
        const spellTags: SpellTagResult[] = SpellTagRepo.fetchSpellTags(userId, bookId, lang);

        return spellTags.map((tag: SpellTagResult): SpellTagProps => ({
            id: tag.tag_id,
            name: System.decryptDataWithUserKey(tag.name, userKey),
            color: tag.color,
        }));
    }

    /**
     * Adds a new spell tag to a book.
     * @param userId - The unique identifier of the user
     * @param bookId - The unique identifier of the book
     * @param name - The name of the tag
     * @param color - The optional color hex code
     * @param existingTagId - Optional existing tag ID for sync
     * @param lang - The language for error messages ('fr' or 'en')
     * @returns The created spell tag props
     */
    static addSpellTag(userId: string, bookId: string, name: string, color: string | null, existingTagId?: string, lang: 'fr' | 'en' = 'fr'): SpellTagProps {
        const userKey: string = getUserEncryptionKey(userId);
        const tagId: string = existingTagId || System.createUniqueId();
        const encryptedName: string = System.encryptDataWithUserKey(name, userKey);
        const nameHash: string = System.hashElement(name);

        SpellTagRepo.insertSpellTag(tagId, bookId, userId, encryptedName, nameHash, color, lang);

        return {
            id: tagId,
            name: name,
            color: color,
        };
    }

    /**
     * Updates an existing spell tag.
     * @param userId - The unique identifier of the user
     * @param tagId - The unique identifier of the tag
     * @param name - The new name of the tag
     * @param color - The new optional color hex code
     * @param lang - The language for error messages ('fr' or 'en')
     * @returns True if the update was successful
     */
    static updateSpellTag(userId: string, tagId: string, name: string, color: string | null, lang: 'fr' | 'en' = 'fr'): boolean {
        const userKey: string = getUserEncryptionKey(userId);
        const encryptedName: string = System.encryptDataWithUserKey(name, userKey);
        const nameHash: string = System.hashElement(name);

        return SpellTagRepo.updateSpellTag(userId, tagId, encryptedName, nameHash, color, lang);
    }

    /**
     * Deletes a spell tag and removes its references from all spells in the book.
     * @param userId - The unique identifier of the user
     * @param tagId - The unique identifier of the tag to delete
     * @param bookId - The unique identifier of the book
     * @param lang - The language for error messages ('fr' or 'en')
     * @returns True if the deletion was successful
     */
    static deleteSpellTag(userId: string, bookId: string, tagId: string, deletedAt: number = System.timeStampInSeconds(), lang: 'fr' | 'en' = 'fr'): boolean {
        const userKey: string = getUserEncryptionKey(userId);

        const spells: SpellResult[] = SpellRepo.fetchSpells(userId, bookId, lang);

        for (const spell of spells) {
            const decryptedTags: string | null = spell.tags ? System.decryptDataWithUserKey(spell.tags, userKey) : null;
            let tagsArray: string[] = [];
            try {
                tagsArray = decryptedTags ? JSON.parse(decryptedTags) as string[] : [];
            } catch {
                tagsArray = [];
            }

            if (tagsArray.includes(tagId)) {
                const updatedTags: string[] = tagsArray.filter((t: string): boolean => t !== tagId);
                const encryptedTags: string = System.encryptDataWithUserKey(JSON.stringify(updatedTags), userKey);
                SpellRepo.updateSpellTags(userId, spell.spell_id, encryptedTags, lang);
            }
        }

        // Then delete the tag
        const deleted: boolean = SpellTagRepo.deleteSpellTag(userId, tagId, lang);
        if (deleted) {
            RemovedItem.deleteTracker(userId, bookId, 'book_spell_tags', tagId, deletedAt, lang);
        }
        return deleted;
    }

    /**
     * Retrieves the spell list with tags for a specific book.
     * @param userId - The unique identifier of the user
     * @param bookId - The unique identifier of the book
     * @param lang - The language for error messages ('fr' or 'en')
     * @returns The spell list response with enabled status, spells, and tags
     */
    static getSpellList(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): SpellListResponse {
        const userKey: string = getUserEncryptionKey(userId);

        const bookTools: BookToolsTable | null = BookRepo.fetchBookTools(userId, bookId, lang);
        const enabled: boolean = bookTools ? bookTools.spells_enabled === 1 : false;

        const spellTags: SpellTagResult[] = SpellTagRepo.fetchSpellTags(userId, bookId, lang);
        const tags: SpellTagProps[] = spellTags.map((tag: SpellTagResult): SpellTagProps => ({
            id: tag.tag_id,
            name: System.decryptDataWithUserKey(tag.name, userKey),
            color: tag.color,
        }));

        const tagMap: Map<string, SpellTagProps> = new Map();
        for (const tag of tags) {
            tagMap.set(tag.id, tag);
        }

        const spellResults: SpellResult[] = SpellRepo.fetchSpells(userId, bookId, lang);

        const spells: SpellListItem[] = spellResults.map((spell: SpellResult): SpellListItem => {
            const decryptedName: string = System.decryptDataWithUserKey(spell.name, userKey);
            const decryptedDescription: string | null = spell.description ? System.decryptDataWithUserKey(spell.description, userKey) : null;
            const decryptedTags: string | null = spell.tags ? System.decryptDataWithUserKey(spell.tags, userKey) : null;

            let tagIds: string[];
            try {
                tagIds = decryptedTags ? JSON.parse(decryptedTags) as string[] : [];
            } catch {
                tagIds = [];
            }

            const resolvedTags: SpellTagProps[] = tagIds
                .map((tagId: string): SpellTagProps | undefined => tagMap.get(tagId))
                .filter((tag: SpellTagProps | undefined): tag is SpellTagProps => tag !== undefined);

            const truncatedDescription: string = decryptedDescription
                ? (decryptedDescription.length > 150 ? decryptedDescription.substring(0, 150) + '...' : decryptedDescription)
                : '';

            return {
                id: spell.spell_id,
                name: decryptedName,
                description: truncatedDescription,
                tags: resolvedTags,
                seriesSpellId: spell.series_spell_id || null,
            };
        });

        return {
            enabled,
            spells,
            tags,
        };
    }

    /**
     * Retrieves the full details of a specific spell.
     * @param userId - The unique identifier of the user
     * @param spellId - The unique identifier of the spell
     * @param lang - The language for error messages ('fr' or 'en')
     * @returns The spell props with all details
     */
    static getSpellDetail(userId: string, spellId: string, lang: 'fr' | 'en' = 'fr'): SpellProps {
        const userKey: string = getUserEncryptionKey(userId);

        const spell: SpellResult | null = SpellRepo.fetchSpellById(userId, spellId, lang);
        if (!spell) {
            throw new Error(lang === 'fr' ? 'Sort non trouvé.' : 'Spell not found.');
        }

        const decryptedName: string = System.decryptDataWithUserKey(spell.name, userKey);
        const decryptedDescription: string | null = spell.description ? System.decryptDataWithUserKey(spell.description, userKey) : null;
        const decryptedAppearance: string | null = spell.appearance ? System.decryptDataWithUserKey(spell.appearance, userKey) : null;
        const decryptedTags: string | null = spell.tags ? System.decryptDataWithUserKey(spell.tags, userKey) : null;

        let tagIds: string[];
        try {
            tagIds = decryptedTags ? JSON.parse(decryptedTags) as string[] : [];
        } catch {
            tagIds = [];
        }

        return {
            id: spell.spell_id,
            name: decryptedName,
            description: decryptedDescription || '',
            appearance: decryptedAppearance || '',
            tags: tagIds,
            powerLevel: spell.power_level ? System.decryptDataWithUserKey(spell.power_level, userKey) : null,
            components: spell.components ? System.decryptDataWithUserKey(spell.components, userKey) : null,
            limitations: spell.limitations ? System.decryptDataWithUserKey(spell.limitations, userKey) : null,
            notes: spell.notes ? System.decryptDataWithUserKey(spell.notes, userKey) : null,
            seriesSpellId: spell.series_spell_id || null,
        };
    }

    /**
     * Adds a new spell to a book.
     * @param userId - The unique identifier of the user
     * @param bookId - The unique identifier of the book
     * @param name - The name of the spell
     * @param description - The description of the spell
     * @param appearance - The appearance of the spell
     * @param tags - The tag IDs array
     * @param powerLevel - The optional power level
     * @param components - The optional components
     * @param limitations - The optional limitations
     * @param notes - The optional notes
     * @param existingSpellId - Optional existing spell ID for sync
     * @param lang - The language for error messages ('fr' or 'en')
     * @returns The created spell props
     */
    static addSpell(userId: string, bookId: string, name: string, description: string, appearance: string, tags: string[], powerLevel: string | null, components: string | null, limitations: string | null, notes: string | null, existingSpellId?: string, lang: 'fr' | 'en' = 'fr', seriesSpellId: string | null = null): SpellProps {
        const userKey: string = getUserEncryptionKey(userId);
        const spellId: string = existingSpellId || System.createUniqueId();

        const encryptedName: string = System.encryptDataWithUserKey(name, userKey);
        const nameHash: string = System.hashElement(name);
        const encryptedDescription: string = System.encryptDataWithUserKey(description, userKey);
        const encryptedAppearance: string = System.encryptDataWithUserKey(appearance, userKey);
        const encryptedTags: string = System.encryptDataWithUserKey(JSON.stringify(tags), userKey);
        const encryptedPowerLevel: string | null = powerLevel ? System.encryptDataWithUserKey(powerLevel, userKey) : null;
        const encryptedComponents: string | null = components ? System.encryptDataWithUserKey(components, userKey) : null;
        const encryptedLimitations: string | null = limitations ? System.encryptDataWithUserKey(limitations, userKey) : null;
        const encryptedNotes: string | null = notes ? System.encryptDataWithUserKey(notes, userKey) : null;

        SpellRepo.insertSpell(
            spellId,
            bookId,
            userId,
            encryptedName,
            nameHash,
            encryptedDescription,
            encryptedAppearance,
            encryptedTags,
            encryptedPowerLevel,
            encryptedComponents,
            encryptedLimitations,
            encryptedNotes,
            lang,
            seriesSpellId,
        );

        return {
            id: spellId,
            name,
            description,
            appearance,
            tags,
            powerLevel,
            components,
            limitations,
            notes,
            seriesSpellId,
        };
    }

    /**
     * Updates an existing spell.
     * @param userId - The unique identifier of the user
     * @param spellId - The unique identifier of the spell
     * @param name - The name of the spell
     * @param description - The description of the spell
     * @param appearance - The appearance of the spell
     * @param tags - The tag IDs array
     * @param powerLevel - The optional power level
     * @param components - The optional components
     * @param limitations - The optional limitations
     * @param notes - The optional notes
     * @param lang - The language for error messages ('fr' or 'en')
     * @returns True if the update was successful
     */
    static updateSpell(userId: string, spellId: string, name: string, description: string, appearance: string, tags: string[], powerLevel: string | null, components: string | null, limitations: string | null, notes: string | null, lang: 'fr' | 'en' = 'fr', seriesSpellId: string | null = null): boolean {
        const userKey: string = getUserEncryptionKey(userId);

        const encryptedName: string = System.encryptDataWithUserKey(name, userKey);
        const nameHash: string = System.hashElement(name);
        const encryptedDescription: string = System.encryptDataWithUserKey(description, userKey);
        const encryptedAppearance: string = System.encryptDataWithUserKey(appearance, userKey);
        const encryptedTags: string = System.encryptDataWithUserKey(JSON.stringify(tags), userKey);
        const encryptedPowerLevel: string | null = powerLevel ? System.encryptDataWithUserKey(powerLevel, userKey) : null;
        const encryptedComponents: string | null = components ? System.encryptDataWithUserKey(components, userKey) : null;
        const encryptedLimitations: string | null = limitations ? System.encryptDataWithUserKey(limitations, userKey) : null;
        const encryptedNotes: string | null = notes ? System.encryptDataWithUserKey(notes, userKey) : null;

        return SpellRepo.updateSpell(
            userId,
            spellId,
            encryptedName,
            nameHash,
            encryptedDescription,
            encryptedAppearance,
            encryptedTags,
            encryptedPowerLevel,
            encryptedComponents,
            encryptedLimitations,
            encryptedNotes,
            lang,
            seriesSpellId,
        );
    }

    /**
     * Deletes a spell.
     * @param userId - The unique identifier of the user
     * @param bookId - The unique identifier of the book
     * @param spellId - The unique identifier of the spell
     * @param deletedAt - The timestamp of deletion
     * @param lang - The language for error messages ('fr' or 'en')
     * @returns True if the deletion was successful
     */
    static deleteSpell(userId: string, bookId: string, spellId: string, deletedAt: number = System.timeStampInSeconds(), lang: 'fr' | 'en' = 'fr'): boolean {
        const deleted: boolean = SpellRepo.deleteSpell(userId, spellId, lang);
        if (deleted) {
            RemovedItem.deleteTracker(userId, bookId, 'book_spells', spellId, deletedAt, lang);
        }
        return deleted;
    }
}
