import LocationRepo, {
    LocationByTagResult,
    LocationElementQueryResult,
    LocationQueryResult
} from "../repositories/location.repository.js";
import System from "../System.js";
import {getUserEncryptionKey} from "../keyManager.js";
import BookRepo, {BookToolsTable} from "../repositories/book.repository.js";
import RemovedItem from "./RemovedItem.js";

export interface SubElement {
    id: string;
    name: string;
    description: string;
}

export interface Element {
    id: string;
    name: string;
    description: string;
    subElements: SubElement[];
}

export interface LocationProps {
    id: string;
    name: string;
    elements: Element[];
    seriesLocationId?: string | null;
}

export interface LocationListResponse {
    locations: LocationProps[];
    enabled: boolean;
}

export interface SyncedLocation {
    id: string;
    name: string;
    lastUpdate: number;
    elements: SyncedLocationElement[];
}

export interface SyncedLocationElement {
    id: string;
    name: string;
    lastUpdate: number;
    subElements: SyncedLocationSubElement[];
}

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

export default class Location {
    /**
     * Retrieves all locations for a given user and book.
     * @param userId - The user's unique identifier.
     * @param bookId - The book's unique identifier.
     * @param lang - The language for error messages ('fr' or 'en'). Defaults to 'fr'.
     * @returns LocationListResponse containing an array of locations and enabled flag.
     */
    static getAllLocations(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): LocationListResponse {
        const bookTools: BookToolsTable | null = BookRepo.fetchBookTools(userId, bookId, lang);
        const enabled: boolean = bookTools ? bookTools.locations_enabled === 1 : false;

        const locationRecords: LocationQueryResult[] = LocationRepo.getLocation(userId, bookId, lang);
        if (!locationRecords || locationRecords.length === 0) {
            return { locations: [], enabled };
        }
        const userKey: string = getUserEncryptionKey(userId);

        const locationArray: LocationProps[] = [];

        for (const record of locationRecords) {
            let location = locationArray.find(loc => loc.id === record.loc_id);

            if (!location) {
                const decryptedName: string = System.decryptDataWithUserKey(record.loc_name, userKey);
                location = {
                    id: record.loc_id,
                    name: decryptedName,
                    elements: [],
                    seriesLocationId: record.series_location_id || null,
                };
                locationArray.push(location);
            }

            if (record.element_id) {
                let element = location.elements.find(elem => elem.id === record.element_id);
                if (!element) {
                    const decryptedName: string = System.decryptDataWithUserKey(record.element_name, userKey);
                    const decryptedDescription: string = record.element_description ? System.decryptDataWithUserKey(record.element_description, userKey) : '';

                    element = {
                        id: record.element_id,
                        name: decryptedName,
                        description: decryptedDescription,
                        subElements: []
                    };
                    location.elements.push(element);
                }

                if (record.sub_element_id) {
                    const subElementExists = element.subElements.some(sub => sub.id === record.sub_element_id);

                    if (!subElementExists) {
                        const decryptedName: string = System.decryptDataWithUserKey(record.sub_elem_name, userKey);
                        const decryptedDescription: string = record.sub_elem_description ? System.decryptDataWithUserKey(record.sub_elem_description, userKey) : '';

                        element.subElements.push({
                            id: record.sub_element_id,
                            name: decryptedName,
                            description: decryptedDescription
                        });
                    }
                }
            }
        }
        return { locations: locationArray, enabled };
    }

    /**
     * Adds a new location section for a book.
     * @param userId - The user's unique identifier.
     * @param locationName - The name of the location to create.
     * @param bookId - The book's unique identifier.
     * @param lang - The language for error messages ('fr' or 'en'). Defaults to 'fr'.
     * @param existingLocationId - Optional existing location ID to use instead of generating a new one.
     * @returns The ID of the created location.
     */
    static addLocationSection(userId: string, locationName: string, bookId: string, lang: 'fr' | 'en' = 'fr', existingLocationId?: string, seriesLocationId: string | null = null): string {
        const userKey: string = getUserEncryptionKey(userId);
        const hashedName: string = System.hashElement(locationName);
        const encryptedName: string = System.encryptDataWithUserKey(locationName, userKey);
        const locationId: string = existingLocationId || System.createUniqueId();
        return LocationRepo.insertLocation(userId, locationId, bookId, encryptedName, hashedName, lang, seriesLocationId);
    }

    /**
     * Adds a new element to a location.
     * @param userId - The user's unique identifier.
     * @param locationId - The parent location's unique identifier.
     * @param elementName - The name of the element to create.
     * @param lang - The language for error messages ('fr' or 'en'). Defaults to 'fr'.
     * @param existingElementId - Optional existing element ID to use instead of generating a new one.
     * @returns The result of the insert operation.
     */
    static addLocationElement(userId: string, locationId: string, elementName: string, lang: 'fr' | 'en' = 'fr', existingElementId?: string): string {
        const userKey: string = getUserEncryptionKey(userId);
        const hashedName: string = System.hashElement(elementName);
        const encryptedName: string = System.encryptDataWithUserKey(elementName, userKey);
        const elementId: string = existingElementId || System.createUniqueId();
        return LocationRepo.insertLocationElement(userId, elementId, locationId, encryptedName, hashedName, lang)
    }

    /**
     * Adds a new sub-element to a location element.
     * @param userId - The user's unique identifier.
     * @param elementId - The parent element's unique identifier.
     * @param subElementName - The name of the sub-element to create.
     * @param lang - The language for error messages ('fr' or 'en'). Defaults to 'fr'.
     * @param existingSubElementId - Optional existing sub-element ID to use instead of generating a new one.
     * @returns The result of the insert operation.
     */
    static addLocationSubElement(userId: string, elementId: string, subElementName: string, lang: 'fr' | 'en' = 'fr', existingSubElementId?: string): string {
        const userKey: string = getUserEncryptionKey(userId);
        const hashedName: string = System.hashElement(subElementName);
        const encryptedName: string = System.encryptDataWithUserKey(subElementName, userKey);
        const subElementId: string = existingSubElementId || System.createUniqueId();
        return LocationRepo.insertLocationSubElement(userId, subElementId, elementId, encryptedName, hashedName, lang)
    }

    /**
     * Updates multiple location sections along with their elements and sub-elements.
     * @param userId - The user's unique identifier.
     * @param locations - Array of location properties to update.
     * @param lang - The language for response messages ('fr' or 'en'). Defaults to 'fr'.
     * @returns An object indicating success and a localized message.
     */
    static updateLocationSection(userId: string, locations: LocationProps[], lang: 'fr' | 'en' = 'fr'): { valid: boolean; message: string } {
        const userKey: string = getUserEncryptionKey(userId);

        for (const location of locations) {
            const hashedLocationName: string = System.hashElement(location.name);
            const encryptedLocationName: string = System.encryptDataWithUserKey(location.name, userKey);
            LocationRepo.updateLocationSection(userId, location.id, encryptedLocationName, hashedLocationName, System.timeStampInSeconds(), lang)
            for (const element of location.elements) {
                const hashedElementName: string = System.hashElement(element.name);
                const encryptedElementName: string = System.encryptDataWithUserKey(element.name, userKey);
                const encryptedElementDescription: string = element.description ? System.encryptDataWithUserKey(element.description, userKey) : '';
                LocationRepo.updateLocationElement(userId, element.id, encryptedElementName, hashedElementName, encryptedElementDescription, System.timeStampInSeconds(), lang)
                for (const subElement of element.subElements) {
                    const hashedSubElementName: string = System.hashElement(subElement.name);
                    const encryptedSubElementName: string = System.encryptDataWithUserKey(subElement.name, userKey);
                    const encryptedSubElementDescription: string = subElement.description ? System.encryptDataWithUserKey(subElement.description, userKey) : '';
                    LocationRepo.updateLocationSubElement(userId, subElement.id, encryptedSubElementName, hashedSubElementName, encryptedSubElementDescription, System.timeStampInSeconds(), lang)
                }
            }
        }
        return {
            valid: true,
            message: lang === 'fr' ? 'Les sections ont été mis à jour.' : 'Sections have been updated.'
        }
    }

    /**
     * Updates a location section with optional name change and series link.
     * @param userId - The unique identifier of the user
     * @param sectionId - The unique identifier of the section
     * @param sectionName - The new name (optional)
     * @param seriesLocationId - The series location ID to link (optional, null to unlink)
     * @param lang - The language for error messages ('fr' or 'en'). Defaults to 'fr'.
     * @returns True if the update was successful
     */
    static updateSectionWithSeriesLink(userId: string, sectionId: string, sectionName?: string, seriesLocationId?: string | null, lang: 'fr' | 'en' = 'fr'): boolean {
        let encryptedName: string | null = null;
        let originalNameHash: string | null = null;

        if (sectionName) {
            const userKey: string = getUserEncryptionKey(userId);
            encryptedName = System.encryptDataWithUserKey(sectionName, userKey);
            originalNameHash = System.hashElement(sectionName);
        }

        return LocationRepo.updateSectionWithSeriesLink(userId, sectionId, encryptedName, originalNameHash, seriesLocationId ?? null, lang);
    }

    /**
     * Deletes a location section and all its associated elements and sub-elements.
     * @param userId - The user's unique identifier.
     * @param bookId - The book's unique identifier.
     * @param locationId - The location's unique identifier to delete.
     * @param deletedAt - The timestamp of deletion.
     * @param lang - The language for error messages ('fr' or 'en'). Defaults to 'fr'.
     * @returns The result of the delete operation.
     */
    static deleteLocationSection(userId: string, bookId: string, locationId: string, deletedAt: number = System.timeStampInSeconds(), lang: 'fr' | 'en' = 'fr'): boolean {
        const deleted: boolean = LocationRepo.deleteLocationSection(userId, locationId, lang);
        if (deleted) {
            RemovedItem.deleteTracker(userId, bookId, 'book_location', locationId, deletedAt, lang);
        }
        return deleted;
    }

    /**
     * Deletes a location element and all its associated sub-elements.
     * @param userId - The user's unique identifier.
     * @param bookId - The book's unique identifier.
     * @param elementId - The element's unique identifier to delete.
     * @param deletedAt - The timestamp of deletion.
     * @param lang - The language for error messages ('fr' or 'en'). Defaults to 'fr'.
     * @returns The result of the delete operation.
     */
    static deleteLocationElement(userId: string, bookId: string, elementId: string, deletedAt: number = System.timeStampInSeconds(), lang: 'fr' | 'en' = 'fr'): boolean {
        const deleted: boolean = LocationRepo.deleteLocationElement(userId, elementId, lang);
        if (deleted) {
            RemovedItem.deleteTracker(userId, bookId, 'location_element', elementId, deletedAt, lang);
        }
        return deleted;
    }

    /**
     * Deletes a location sub-element.
     * @param userId - The user's unique identifier.
     * @param bookId - The book's unique identifier.
     * @param subElementId - The sub-element's unique identifier to delete.
     * @param deletedAt - The timestamp of deletion.
     * @param lang - The language for error messages ('fr' or 'en'). Defaults to 'fr'.
     * @returns The result of the delete operation.
     */
    static deleteLocationSubElement(userId: string, bookId: string, subElementId: string, deletedAt: number = System.timeStampInSeconds(), lang: 'fr' | 'en' = 'fr'): boolean {
        const deleted: boolean = LocationRepo.deleteLocationSubElement(userId, subElementId, lang);
        if (deleted) {
            RemovedItem.deleteTracker(userId, bookId, 'location_sub_element', subElementId, deletedAt, lang);
        }
        return deleted;
    }

    /**
     * Retrieves location tags (elements or sub-elements) for tagging purposes.
     * Returns sub-elements when an element has multiple sub-elements, otherwise returns the element itself.
     * @param userId - The user's unique identifier.
     * @param bookId - The book's unique identifier.
     * @param lang - The language for error messages ('fr' or 'en'). Defaults to 'fr'.
     * @returns An array of sub-elements suitable for tagging.
     */
    static getLocationTags(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): SubElement[] {
        const tagRecords: LocationElementQueryResult[] = LocationRepo.fetchLocationTags(userId, bookId, lang);
        if (!tagRecords || tagRecords.length === 0) return [];
        const userKey: string = getUserEncryptionKey(userId);

        const elementCounts = new Map<string, number>();
        tagRecords.forEach((record: LocationElementQueryResult): void => {
            elementCounts.set(record.element_id, (elementCounts.get(record.element_id) || 0) + 1);
        });

        const subElements: SubElement[] = [];
        const processedIds = new Set<string>();

        for (const record of tagRecords) {
            const elementCount: number = elementCounts.get(record.element_id) || 0;

            if (elementCount > 1 && record.sub_element_id) {
                if (processedIds.has(record.sub_element_id)) continue;

                subElements.push({
                    id: record.sub_element_id,
                    name: System.decryptDataWithUserKey(record.sub_elem_name, userKey),
                    description: record.sub_elem_description ? System.decryptDataWithUserKey(record.sub_elem_description, userKey) : ''
                });
                processedIds.add(record.sub_element_id);
            } else if (elementCount === 1 && !record.sub_element_id) {
                if (processedIds.has(record.element_id)) continue;

                subElements.push({
                    id: record.element_id,
                    name: System.decryptDataWithUserKey(record.element_name, userKey),
                    description: record.element_description ? System.decryptDataWithUserKey(record.element_description, userKey) : ''
                });
                processedIds.add(record.element_id);
            }
        }
        return subElements;
    }

    /**
     * Retrieves location elements filtered by specific tag IDs.
     * @param userId - The user's unique identifier.
     * @param locations - Array of location tag IDs to filter by.
     * @param lang - The language for error messages ('fr' or 'en'). Defaults to 'fr'.
     * @returns An array of elements with their associated sub-elements.
     */
    static getLocationsByTags(userId: string, locations: string[], lang: 'fr' | 'en' = 'fr'): Element[] {
        const locationTagRecords: LocationByTagResult[] = LocationRepo.fetchLocationsByTags(userId, locations, lang);
        if (!locationTagRecords || locationTagRecords.length === 0) return [];
        const userKey: string = getUserEncryptionKey(userId);
        const locationElements: Element[] = [];
        for (const record of locationTagRecords) {
            let element: Element | undefined = locationElements.find((elem: Element): boolean => elem.name === record.element_name);
            if (!element) {
                const decryptedName: string = System.decryptDataWithUserKey(record.element_name, userKey);
                const decryptedDescription: string = record.element_description ? System.decryptDataWithUserKey(record.element_description, userKey) : '';
                element = {
                    id: '',
                    name: decryptedName,
                    description: decryptedDescription,
                    subElements: []
                };
                locationElements.push(element);
            }
            if (record.sub_elem_name) {
                const subElementExists: boolean = element.subElements.some(sub => sub.name === record.sub_elem_name);
                if (!subElementExists) {
                    const decryptedName: string = System.decryptDataWithUserKey(record.sub_elem_name, userKey);
                    const decryptedDescription: string = record.sub_elem_description ? System.decryptDataWithUserKey(record.sub_elem_description, userKey) : '';
                    element.subElements.push({
                        id: '',
                        name: decryptedName,
                        description: decryptedDescription
                    });
                }
            }
        }
        return locationElements;
    }

    /**
     * Generates a formatted description string from an array of location elements.
     * @param locations - Array of location elements to describe.
     * @returns A formatted string with location names and descriptions.
     */
    static locationsDescription(locations: Element[]): string {
        return locations.map((location: Element): string => {
            const descriptionFields: string[] = [];
            if (location.name) descriptionFields.push(`Nom : ${location.name}`);
            if (location.description) descriptionFields.push(`Description : ${location.description}`);
            return descriptionFields.join('\n');
        }).join('\n\n');
    }

}
