import CharacterRepo, {
    AttributeResult,
    CharacterResult,
    CompleteCharacterResult
} from "../repositories/character.repository.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 type CharacterCategory = 'Main' | 'Secondary' | 'Recurring';

export interface CharacterPropsPost {
    id: string | null;
    name: string;
    lastName: string;
    nickname: string;
    age: number | null;
    gender: string;
    species: string;
    nationality: string;
    status: string;
    category: CharacterCategory;
    title: string;
    image: string;
    physical: { name: string }[];
    psychological: { name: string }[];
    relations: { name: string }[];
    skills: { name: string }[];
    weaknesses: { name: string }[];
    strengths: { name: string }[];
    goals: { name: string }[];
    motivations: { name: string }[];
    arc: { name: string }[];
    secrets: { name: string }[];
    fears: { name: string }[];
    flaws: { name: string }[];
    beliefs: { name: string }[];
    conflicts: { name: string }[];
    quotes: { name: string }[];
    distinguishingMarks: { name: string }[];
    items: { name: string }[];
    affiliations: { name: string }[];
    role: string;
    biography?: string;
    history?: string;
    speechPattern?: string;
    catchphrase?: string;
    residence?: string;
    notes?: string;
    color?: string;
    seriesCharacterId?: string | null;
}


export interface CharacterProps {
    id: string;
    name: string;
    lastName: string;
    nickname: string;
    age: number | null;
    gender: string;
    species: string;
    nationality: string;
    status: string;
    title: string;
    category: string;
    image: string;
    role: string;
    biography: string;
    history: string;
    speechPattern: string;
    catchphrase: string;
    residence: string;
    notes: string;
    color: string;
    seriesCharacterId: string | null;
}

export interface CharacterListResponse {
    characters: CharacterProps[];
    enabled: boolean;
}

export interface CompleteCharacterProps {
    id?: string;
    name: string;
    lastName: string;
    nickname: string;
    age: number | null;
    gender: string;
    species: string;
    nationality: string;
    status: string;
    title: string;
    category: string;
    image?: string;
    role: string;
    biography: string;
    history: string;
    speechPattern: string;
    catchphrase: string;
    residence: string;
    notes: string;
    color: string;
    [key: string]: Attribute[] | string | number | null | undefined;
}

export interface Attribute {
    id: string;
    name: string;
}


export interface CharacterAttribute {
    type: string;
    values: Attribute[];
}

export interface SyncedCharacter {
    id: string;
    name: string;
    lastUpdate: number;
    attributes: SyncedCharacterAttribute[];
}

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

export default class Character {
    /**
     * Retrieves a list of all characters for a specific book.
     * Decrypts character data using the user's encryption key.
     * @param userId - The unique identifier of the user
     * @param bookId - The unique identifier of the book
     * @param lang - The language code for localization (defaults to 'fr')
     * @returns An array of decrypted character properties
     */
    public static getCharacterList(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): CharacterListResponse {
        const bookTools: BookToolsTable | null = BookRepo.fetchBookTools(userId, bookId, lang);
        const enabled: boolean = bookTools ? bookTools.characters_enabled === 1 : false;

        const userEncryptionKey: string = getUserEncryptionKey(userId);
        const encryptedCharacters: CharacterResult[] = CharacterRepo.fetchCharacters(userId, bookId, lang);
        if (!encryptedCharacters || encryptedCharacters.length === 0) {
            return { characters: [], enabled };
        }
        const decryptedCharacterList: CharacterProps[] = [];
        for (const encryptedCharacter of encryptedCharacters) {
            decryptedCharacterList.push({
                id: encryptedCharacter.character_id,
                name: encryptedCharacter.first_name ? System.decryptDataWithUserKey(encryptedCharacter.first_name, userEncryptionKey) : '',
                lastName: encryptedCharacter.last_name ? System.decryptDataWithUserKey(encryptedCharacter.last_name, userEncryptionKey) : '',
                nickname: encryptedCharacter.nickname ? System.decryptDataWithUserKey(encryptedCharacter.nickname, userEncryptionKey) : '',
                age: encryptedCharacter.age ? parseInt(System.decryptDataWithUserKey(encryptedCharacter.age, userEncryptionKey), 10) : null,
                gender: encryptedCharacter.gender ? System.decryptDataWithUserKey(encryptedCharacter.gender, userEncryptionKey) : '',
                species: encryptedCharacter.species ? System.decryptDataWithUserKey(encryptedCharacter.species, userEncryptionKey) : '',
                nationality: encryptedCharacter.nationality ? System.decryptDataWithUserKey(encryptedCharacter.nationality, userEncryptionKey) : '',
                status: encryptedCharacter.status ? System.decryptDataWithUserKey(encryptedCharacter.status, userEncryptionKey) : 'alive',
                title: encryptedCharacter.title ? System.decryptDataWithUserKey(encryptedCharacter.title, userEncryptionKey) : '',
                category: encryptedCharacter.category ? System.decryptDataWithUserKey(encryptedCharacter.category, userEncryptionKey) : '',
                image: encryptedCharacter.image ? System.decryptDataWithUserKey(encryptedCharacter.image, userEncryptionKey) : '',
                role: encryptedCharacter.role ? System.decryptDataWithUserKey(encryptedCharacter.role, userEncryptionKey) : '',
                biography: encryptedCharacter.biography ? System.decryptDataWithUserKey(encryptedCharacter.biography, userEncryptionKey) : '',
                history: encryptedCharacter.history ? System.decryptDataWithUserKey(encryptedCharacter.history, userEncryptionKey) : '',
                speechPattern: encryptedCharacter.speech_pattern ? System.decryptDataWithUserKey(encryptedCharacter.speech_pattern, userEncryptionKey) : '',
                catchphrase: encryptedCharacter.catchphrase ? System.decryptDataWithUserKey(encryptedCharacter.catchphrase, userEncryptionKey) : '',
                residence: encryptedCharacter.residence ? System.decryptDataWithUserKey(encryptedCharacter.residence, userEncryptionKey) : '',
                notes: encryptedCharacter.notes ? System.decryptDataWithUserKey(encryptedCharacter.notes, userEncryptionKey) : '',
                color: encryptedCharacter.color ? System.decryptDataWithUserKey(encryptedCharacter.color, userEncryptionKey) : '',
                seriesCharacterId: encryptedCharacter.series_character_id || null,
            })
        }
        return { characters: decryptedCharacterList, enabled };
    }

    /**
     * Creates a new character with all its attributes for a specific book.
     * Encrypts all character data before storing in the database.
     * @param userId - The unique identifier of the user
     * @param character - The character data to be created
     * @param bookId - The unique identifier of the book
     * @param lang - The language code for localization (defaults to 'fr')
     * @param existingCharacterId - Optional existing character ID for updates or imports
     * @returns The unique identifier of the newly created character
     */
    public static addNewCharacter(userId: string, character: CharacterPropsPost, bookId: string, lang: 'fr' | 'en' = 'fr', existingCharacterId?: string): string {
        const userEncryptionKey: string = getUserEncryptionKey(userId);
        const characterId: string = existingCharacterId || System.createUniqueId();

        const characterData = {
            firstName: System.encryptDataWithUserKey(character.name, userEncryptionKey),
            lastName: System.encryptDataWithUserKey(character.lastName, userEncryptionKey),
            nickname: System.encryptDataWithUserKey(character.nickname || '', userEncryptionKey),
            age: character.age !== null ? System.encryptDataWithUserKey(String(character.age), userEncryptionKey) : '',
            gender: System.encryptDataWithUserKey(character.gender || '', userEncryptionKey),
            species: System.encryptDataWithUserKey(character.species || '', userEncryptionKey),
            nationality: System.encryptDataWithUserKey(character.nationality || '', userEncryptionKey),
            status: System.encryptDataWithUserKey(character.status || 'alive', userEncryptionKey),
            title: System.encryptDataWithUserKey(character.title, userEncryptionKey),
            category: System.encryptDataWithUserKey(character.category, userEncryptionKey),
            image: System.encryptDataWithUserKey(character.image, userEncryptionKey),
            role: System.encryptDataWithUserKey(character.role, userEncryptionKey),
            biography: System.encryptDataWithUserKey(character.biography || '', userEncryptionKey),
            history: System.encryptDataWithUserKey(character.history || '', userEncryptionKey),
            speechPattern: System.encryptDataWithUserKey(character.speechPattern || '', userEncryptionKey),
            catchphrase: System.encryptDataWithUserKey(character.catchphrase || '', userEncryptionKey),
            residence: System.encryptDataWithUserKey(character.residence || '', userEncryptionKey),
            notes: System.encryptDataWithUserKey(character.notes || '', userEncryptionKey),
            color: System.encryptDataWithUserKey(character.color || '', userEncryptionKey),
        };

        CharacterRepo.addNewCharacter(userId, characterId, characterData, bookId, lang, character.seriesCharacterId || null);
        const characterPropertyKeys: string[] = Object.keys(character);
        for (const propertyKey of characterPropertyKeys) {
            if (Array.isArray(character[propertyKey as keyof CharacterPropsPost])) {
                const attributeArray = character[propertyKey as keyof CharacterPropsPost] as { name: string }[];
                if (attributeArray.length > 0) {
                    for (const attributeItem of attributeArray) {
                        const attributeType: string = propertyKey;
                        const attributeName: string = attributeItem.name;
                        this.addNewAttribute(characterId, userId, attributeType, attributeName, lang);
                    }
                }
            }
        }
        return characterId;
    }

    /**
     * Updates an existing character's core properties.
     * Encrypts all updated data before storing in the database.
     * @param userId - The unique identifier of the user
     * @param character - The character data with updated values
     * @param lang - The language code for localization (defaults to 'fr')
     * @returns True if the update was successful, false otherwise
     */
    static updateCharacter(userId: string, character: CharacterPropsPost, lang: 'fr' | 'en' = 'fr'): boolean {
        const userEncryptionKey: string = getUserEncryptionKey(userId);
        if (!character.id) {
            return false;
        }

        const characterData = {
            firstName: System.encryptDataWithUserKey(character.name, userEncryptionKey),
            lastName: System.encryptDataWithUserKey(character.lastName, userEncryptionKey),
            nickname: System.encryptDataWithUserKey(character.nickname || '', userEncryptionKey),
            age: character.age !== null ? System.encryptDataWithUserKey(String(character.age), userEncryptionKey) : '',
            gender: System.encryptDataWithUserKey(character.gender || '', userEncryptionKey),
            species: System.encryptDataWithUserKey(character.species || '', userEncryptionKey),
            nationality: System.encryptDataWithUserKey(character.nationality || '', userEncryptionKey),
            status: System.encryptDataWithUserKey(character.status || 'alive', userEncryptionKey),
            title: System.encryptDataWithUserKey(character.title, userEncryptionKey),
            category: System.encryptDataWithUserKey(character.category, userEncryptionKey),
            image: System.encryptDataWithUserKey(character.image, userEncryptionKey),
            role: System.encryptDataWithUserKey(character.role, userEncryptionKey),
            biography: System.encryptDataWithUserKey(character.biography || '', userEncryptionKey),
            history: System.encryptDataWithUserKey(character.history || '', userEncryptionKey),
            speechPattern: System.encryptDataWithUserKey(character.speechPattern || '', userEncryptionKey),
            catchphrase: System.encryptDataWithUserKey(character.catchphrase || '', userEncryptionKey),
            residence: System.encryptDataWithUserKey(character.residence || '', userEncryptionKey),
            notes: System.encryptDataWithUserKey(character.notes || '', userEncryptionKey),
            color: System.encryptDataWithUserKey(character.color || '', userEncryptionKey),
        };

        return CharacterRepo.updateCharacter(userId, character.id, characterData, System.timeStampInSeconds(), lang, character.seriesCharacterId || null);
    }

    /**
     * Adds a new attribute to a character.
     * Attributes are categorized properties like physical traits, skills, or goals.
     * @param characterId - The unique identifier of the character
     * @param userId - The unique identifier of the user
     * @param type - The type/category of the attribute (e.g., 'physical', 'skills')
     * @param name - The value/name of the attribute
     * @param lang - The language code for localization (defaults to 'fr')
     * @param existingAttributeId - Optional existing attribute ID for updates or imports
     * @returns The unique identifier of the newly created attribute
     */
    static addNewAttribute(characterId: string, userId: string, type: string, name: string, lang: 'fr' | 'en' = 'fr', existingAttributeId?: string): string {
        const userEncryptionKey: string = getUserEncryptionKey(userId);
        const attributeId: string = existingAttributeId || System.createUniqueId();
        const encryptedType: string = System.encryptDataWithUserKey(type, userEncryptionKey);
        const encryptedName: string = System.encryptDataWithUserKey(name, userEncryptionKey);
        return CharacterRepo.insertAttribute(attributeId, characterId, userId, encryptedType, encryptedName, lang);
    }

    /**
     * Deletes an attribute from a character.
     * @param userId - The unique identifier of the user
     * @param bookId - The unique identifier of the book
     * @param attributeId - The unique identifier of the attribute to delete
     * @param deletedAt - The timestamp of deletion
     * @param lang - The language code for localization (defaults to 'fr')
     * @returns True if the deletion was successful, false otherwise
     */
    static deleteAttribute(userId: string, bookId: string, attributeId: string, deletedAt: number = System.timeStampInSeconds(), lang: 'fr' | 'en' = 'fr'): boolean {
        const deleted: boolean = CharacterRepo.deleteAttribute(userId, attributeId, lang);
        if (deleted) {
            RemovedItem.deleteTracker(userId, bookId, 'book_characters_attributes', attributeId, deletedAt, lang);
        }
        return deleted;
    }

    /**
     * Deletes a character and all its related data.
     * @param userId - The unique identifier of the user
     * @param bookId - The unique identifier of the book
     * @param characterId - The unique identifier of the character to delete
     * @param deletedAt - The timestamp of deletion
     * @param lang - The language code for localization (defaults to 'fr')
     * @returns True if the deletion was successful
     */
    static deleteCharacter(userId: string, bookId: string, characterId: string, deletedAt: number = System.timeStampInSeconds(), lang: 'fr' | 'en' = 'fr'): boolean {
        const deleted: boolean = CharacterRepo.deleteCharacter(userId, characterId, lang);
        if (deleted) {
            RemovedItem.deleteTracker(userId, bookId, 'book_characters', characterId, deletedAt, lang);
        }
        return deleted;
    }

    /**
     * Retrieves all attributes for a specific character, grouped by type.
     * Decrypts attribute data using the user's encryption key.
     * @param characterId - The unique identifier of the character
     * @param userId - The unique identifier of the user
     * @param lang - The language code for localization (defaults to 'fr')
     * @returns An array of character attributes grouped by type
     */
    static getAttributes(characterId: string, userId: string, lang: 'fr' | 'en' = 'fr'): CharacterAttribute[] {
        const userEncryptionKey: string = getUserEncryptionKey(userId);
        const encryptedAttributes: AttributeResult[] = CharacterRepo.fetchAttributes(characterId, userId, lang);
        if (!encryptedAttributes?.length) return [];

        const attributesByType: Map<string, Attribute[]> = new Map<string, Attribute[]>();

        for (const encryptedAttribute of encryptedAttributes) {
            const decryptedType: string = System.decryptDataWithUserKey(encryptedAttribute.attribute_name, userEncryptionKey);
            const decryptedValue: string = encryptedAttribute.attribute_value ? System.decryptDataWithUserKey(encryptedAttribute.attribute_value, userEncryptionKey) : '';

            if (!attributesByType.has(decryptedType)) {
                attributesByType.set(decryptedType, []);
            }

            attributesByType.get(decryptedType)!.push({
                id: encryptedAttribute.attr_id,
                name: decryptedValue
            });
        }

        return Array.from<[string, Attribute[]], CharacterAttribute>(
            attributesByType,
            ([type, values]: [string, Attribute[]]): CharacterAttribute => ({type, values})
        );
    }

    /**
     * Retrieves complete character data including all attributes for multiple characters.
     * Used for exporting or displaying full character profiles.
     * @param userId - The unique identifier of the user
     * @param bookId - The unique identifier of the book
     * @param characters - An array of character IDs to retrieve
     * @param lang - The language code for localization (defaults to 'fr')
     * @returns An array of complete character objects with all their attributes
     */
    static getCompleteCharacterList(userId: string, bookId: string, characters: string[], lang: 'fr' | 'en' = 'fr'): CompleteCharacterProps[] {
        const encryptedCharacterList: CompleteCharacterResult[] = CharacterRepo.fetchCompleteCharacters(userId, bookId, characters, lang);

        if (!encryptedCharacterList || encryptedCharacterList.length === 0) {
            return [];
        }

        const userEncryptionKey: string = getUserEncryptionKey(userId);
        const completeCharactersMap = new Map<string, CompleteCharacterProps>();
        for (const encryptedCharacter of encryptedCharacterList) {
            if (!encryptedCharacter.character_id) {
                continue;
            }

            if (!completeCharactersMap.has(encryptedCharacter.character_id)) {
                const decryptedCharacter: CompleteCharacterProps = {
                    id: '',
                    name: encryptedCharacter.first_name ? System.decryptDataWithUserKey(encryptedCharacter.first_name, userEncryptionKey) : '',
                    lastName: encryptedCharacter.last_name ? System.decryptDataWithUserKey(encryptedCharacter.last_name, userEncryptionKey) : '',
                    nickname: encryptedCharacter.nickname ? System.decryptDataWithUserKey(encryptedCharacter.nickname as string, userEncryptionKey) : '',
                    age: encryptedCharacter.age ? parseInt(System.decryptDataWithUserKey(encryptedCharacter.age as string, userEncryptionKey), 10) : null,
                    gender: encryptedCharacter.gender ? System.decryptDataWithUserKey(encryptedCharacter.gender as string, userEncryptionKey) : '',
                    species: encryptedCharacter.species ? System.decryptDataWithUserKey(encryptedCharacter.species as string, userEncryptionKey) : '',
                    nationality: encryptedCharacter.nationality ? System.decryptDataWithUserKey(encryptedCharacter.nationality as string, userEncryptionKey) : '',
                    status: encryptedCharacter.status ? System.decryptDataWithUserKey(encryptedCharacter.status as string, userEncryptionKey) : 'alive',
                    title: encryptedCharacter.title ? System.decryptDataWithUserKey(encryptedCharacter.title as string, userEncryptionKey) : '',
                    category: encryptedCharacter.category ? System.decryptDataWithUserKey(encryptedCharacter.category as string, userEncryptionKey) : '',
                    role: encryptedCharacter.role ? System.decryptDataWithUserKey(encryptedCharacter.role as string, userEncryptionKey) : '',
                    biography: encryptedCharacter.biography ? System.decryptDataWithUserKey(encryptedCharacter.biography as string, userEncryptionKey) : '',
                    history: encryptedCharacter.history ? System.decryptDataWithUserKey(encryptedCharacter.history as string, userEncryptionKey) : '',
                    speechPattern: encryptedCharacter.speech_pattern ? System.decryptDataWithUserKey(encryptedCharacter.speech_pattern as string, userEncryptionKey) : '',
                    catchphrase: encryptedCharacter.catchphrase ? System.decryptDataWithUserKey(encryptedCharacter.catchphrase as string, userEncryptionKey) : '',
                    residence: encryptedCharacter.residence ? System.decryptDataWithUserKey(encryptedCharacter.residence as string, userEncryptionKey) : '',
                    notes: encryptedCharacter.notes ? System.decryptDataWithUserKey(encryptedCharacter.notes as string, userEncryptionKey) : '',
                    color: encryptedCharacter.color ? System.decryptDataWithUserKey(encryptedCharacter.color as string, userEncryptionKey) : '',
                    physical: [],
                    psychological: [],
                    relations: [],
                    skills: [],
                    weaknesses: [],
                    strengths: [],
                    goals: [],
                    motivations: [],
                    arc: [],
                    secrets: [],
                    fears: [],
                    flaws: [],
                    beliefs: [],
                    conflicts: [],
                    quotes: [],
                    distinguishingMarks: [],
                    items: [],
                    affiliations: []
                };
                completeCharactersMap.set(encryptedCharacter.character_id, decryptedCharacter);
            }

            const characterEntry: CompleteCharacterProps | undefined = completeCharactersMap.get(encryptedCharacter.character_id);

            if (!encryptedCharacter.attribute_name || !characterEntry) {
                continue;
            }
            const decryptedAttributeName: string = System.decryptDataWithUserKey(encryptedCharacter.attribute_name, userEncryptionKey);
            const decryptedAttributeValue: string = encryptedCharacter.attribute_value ? System.decryptDataWithUserKey(encryptedCharacter.attribute_value, userEncryptionKey) : '';

            if (Array.isArray(characterEntry[decryptedAttributeName])) {
                characterEntry[decryptedAttributeName].push({
                    id: '',
                    name: decryptedAttributeValue
                });
            }
        }
        return Array.from(completeCharactersMap.values());
    }

    /**
     * Generates a formatted vCard-style string representation of characters.
     * Useful for AI context or text-based exports.
     * @param characters - An array of complete character objects to format
     * @returns A formatted string containing all character information
     */
    static characterVCard(characters: CompleteCharacterProps[]): string {
        const uniqueCharactersMap = new Map<string, CompleteCharacterProps>();
        let formattedCharactersDescription: string = '';

        characters.forEach((character: CompleteCharacterProps): void => {
            const characterIdentifier: string = character.name || character.id || 'unknown';

            if (!uniqueCharactersMap.has(characterIdentifier)) {
                uniqueCharactersMap.set(characterIdentifier, {
                    name: character.name,
                    lastName: character.lastName,
                    nickname: character.nickname,
                    age: character.age,
                    gender: character.gender,
                    species: character.species,
                    nationality: character.nationality,
                    status: character.status,
                    title: character.title,
                    category: character.category,
                    role: character.role,
                    biography: character.biography,
                    history: character.history,
                    speechPattern: character.speechPattern,
                    catchphrase: character.catchphrase,
                    residence: character.residence,
                    notes: character.notes,
                    color: character.color
                });
            }

            const aggregatedCharacterData: CompleteCharacterProps = uniqueCharactersMap.get(characterIdentifier)!;

            Object.keys(character).forEach((propertyName: string): void => {
                if (Array.isArray(character[propertyName])) {
                    if (!aggregatedCharacterData[propertyName]) aggregatedCharacterData[propertyName] = [];
                    (aggregatedCharacterData[propertyName] as Attribute[]).push(...(character[propertyName] as Attribute[]));
                }
            });
        });

        formattedCharactersDescription = Array.from(uniqueCharactersMap.values()).map((character: CompleteCharacterProps): string => {
            const characterDescriptionLines: string[] = [];
            const fullName: string = [character.name, character.lastName].filter(Boolean).join(' ');
            if (fullName) characterDescriptionLines.push(`Nom : ${fullName}`);

            (['category', 'title', 'role', 'biography', 'history'] as const).forEach((propertyKey) => {
                if (character[propertyKey]) {
                    characterDescriptionLines.push(`${propertyKey.charAt(0).toUpperCase() + propertyKey.slice(1)} : ${character[propertyKey]}`);
                }
            });

            Object.keys(character).forEach((propertyKey: string): void => {
                const propertyValue = character[propertyKey];
                if (Array.isArray(propertyValue) && propertyValue.length > 0) {
                    const capitalizedPropertyKey: string = propertyKey.charAt(0).toUpperCase() + propertyKey.slice(1);
                    const formattedAttributeValues: string = propertyValue.map((attributeItem: Attribute) => attributeItem.name).join(', ');
                    characterDescriptionLines.push(`${capitalizedPropertyKey} : ${formattedAttributeValues}`);
                }
            });

            return characterDescriptionLines.join('\n');
        }).join('\n\n');
        return formattedCharactersDescription;
    }

}
