import { getUserEncryptionKey } from "../keyManager.js";
import System from "../System.js";
import SeriesRepo, { SeriesBookResult, SeriesListItem, SeriesResult } from "../repositories/series.repo.js";
import RemovedItem from "./RemovedItem.js";

export interface SeriesProps {
    id: string;
    name: string;
    description: string;
    coverImage: string | null;
}

export interface SeriesDetailProps {
    id: string;
    name: string;
    description: string;
    coverImage: string | null;
    books: SeriesBookProps[];
}

export interface SeriesBookProps {
    bookId: string;
    title: string;
    order: number;
    coverImage: string | null;
}

export interface SeriesListItemProps {
    id: string;
    name: string;
    description: string;
    coverImage: string | null;
    bookCount: number;
    bookIds: string[];
}

export interface BooksOrderPost {
    bookId: string;
    order: number;
}

export default class Series {
    /**
     * Gets the list of all series for a user.
     * @param userId - The unique identifier of the user
     * @param lang - The language for error messages ('fr' or 'en')
     * @returns The list of series with decrypted names and descriptions
     */
    public static getSeriesList(userId: string, lang: 'fr' | 'en' = 'fr'): SeriesListItemProps[] {
        const userKey: string = getUserEncryptionKey(userId);
        const seriesResults: SeriesListItem[] = SeriesRepo.fetchUserSeries(userId, lang);

        return seriesResults.map((seriesItem: SeriesListItem): SeriesListItemProps => ({
            id: seriesItem.series_id,
            name: System.decryptDataWithUserKey(seriesItem.name, userKey),
            description: seriesItem.description ? System.decryptDataWithUserKey(seriesItem.description, userKey) : '',
            coverImage: seriesItem.cover_image,
            bookCount: seriesItem.book_count,
            bookIds: seriesItem.book_ids ? seriesItem.book_ids.split(',') : []
        }));
    }

    /**
     * Gets the detail of a series including its books.
     * @param userId - The unique identifier of the user
     * @param seriesId - The unique identifier of the series
     * @param lang - The language for error messages ('fr' or 'en')
     * @returns The series detail with decrypted data
     */
    public static getSeriesDetail(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesDetailProps {
        const userKey: string = getUserEncryptionKey(userId);

        const seriesResult: SeriesResult | null = SeriesRepo.fetchSeriesById(userId, seriesId, lang);
        if (!seriesResult) {
            throw new Error(lang === 'fr' ? 'Série non trouvée.' : 'Series not found.');
        }

        const booksResult: SeriesBookResult[] = SeriesRepo.fetchSeriesBooks(userId, seriesId, lang);

        const books: SeriesBookProps[] = booksResult.map((book: SeriesBookResult) => ({
            bookId: book.book_id,
            title: System.decryptDataWithUserKey(book.title, userKey),
            order: book.book_order,
            coverImage: book.cover_image
        }));

        return {
            id: seriesResult.series_id,
            name: System.decryptDataWithUserKey(seriesResult.name, userKey),
            description: seriesResult.description ? System.decryptDataWithUserKey(seriesResult.description, userKey) : '',
            coverImage: seriesResult.cover_image,
            books
        };
    }

    /**
     * Creates a new series.
     * @param userId - The unique identifier of the user
     * @param name - The name of the series
     * @param description - The description of the series
     * @param lang - The language for error messages ('fr' or 'en')
     * @param bookIds - Optional array of book IDs to add to the series
     * @returns The created series ID
     */
    public static createSeries(userId: string, name: string, description: string, lang: 'fr' | 'en' = 'fr', bookIds?: string[]): string {
        const userKey: string = getUserEncryptionKey(userId);
        const seriesId: string = System.createUniqueId();

        const encryptedName: string = System.encryptDataWithUserKey(name, userKey);
        const hashedName: string = System.hashElement(name);
        const encryptedDescription: string | null = description ? System.encryptDataWithUserKey(description, userKey) : null;

        SeriesRepo.insertSeries(seriesId, userId, encryptedName, hashedName, encryptedDescription, lang);

        if (bookIds && bookIds.length > 0) {
            for (let i: number = 0; i < bookIds.length; i++) {
                SeriesRepo.addBookToSeries(seriesId, bookIds[i], i + 1, lang);
            }
        }

        return seriesId;
    }

    /**
     * Updates an existing series.
     * @param userId - The unique identifier of the user
     * @param seriesId - The unique identifier of the series
     * @param name - The name of the series
     * @param description - The description of the series
     * @param lang - The language for error messages ('fr' or 'en')
     * @returns True if the update was successful
     */
    public static updateSeries(userId: string, seriesId: string, name: string, description: string, lang: 'fr' | 'en' = 'fr'): boolean {
        const exists: boolean = SeriesRepo.isSeriesExist(userId, seriesId, lang);
        if (!exists) {
            throw new Error(lang === 'fr' ? 'Série non trouvée.' : 'Series not found.');
        }

        const userKey: string = getUserEncryptionKey(userId);

        const encryptedName: string = System.encryptDataWithUserKey(name, userKey);
        const hashedName: string = System.hashElement(name);
        const encryptedDescription: string | null = description ? System.encryptDataWithUserKey(description, userKey) : null;

        return SeriesRepo.updateSeries(userId, seriesId, encryptedName, hashedName, encryptedDescription, lang);
    }

    /**
     * Deletes a series.
     * @param userId - The unique identifier of the user
     * @param seriesId - The unique identifier of the series
     * @param deletedAt - The timestamp of deletion
     * @param lang - The language for error messages ('fr' or 'en')
     * @returns True if the deletion was successful
     */
    public static deleteSeries(userId: string, seriesId: string, deletedAt: number = System.timeStampInSeconds(), lang: 'fr' | 'en' = 'fr'): boolean {
        const exists: boolean = SeriesRepo.isSeriesExist(userId, seriesId, lang);
        if (!exists) {
            throw new Error(lang === 'fr' ? 'Série non trouvée.' : 'Series not found.');
        }

        const deleted: boolean = SeriesRepo.deleteSeries(userId, seriesId, lang);
        if (deleted) {
            RemovedItem.deleteTracker(userId, null, 'book_series', seriesId, deletedAt, lang);
        }
        return deleted;
    }

    /**
     * Adds a book to a series.
     * @param userId - The unique identifier of the user
     * @param seriesId - The unique identifier of the series
     * @param bookId - The unique identifier of the book
     * @param order - The order of the book in the series
     * @param lang - The language for error messages ('fr' or 'en')
     * @returns True if the addition was successful
     */
    public static addBookToSeries(userId: string, seriesId: string, bookId: string, order: number, lang: 'fr' | 'en' = 'fr'): boolean {
        const exists: boolean = SeriesRepo.isSeriesExist(userId, seriesId, lang);
        if (!exists) {
            throw new Error(lang === 'fr' ? 'Série non trouvée.' : 'Series not found.');
        }

        return SeriesRepo.addBookToSeries(seriesId, bookId, order, lang);
    }

    /**
     * Removes a book from a series.
     * @param userId - The unique identifier of the user
     * @param seriesId - The unique identifier of the series
     * @param bookId - The unique identifier of the book
     * @param deletedAt - The timestamp of deletion
     * @param lang - The language for error messages ('fr' or 'en')
     * @returns True if the removal was successful
     */
    public static removeBookFromSeries(userId: string, seriesId: string, bookId: string, deletedAt: number = System.timeStampInSeconds(), lang: 'fr' | 'en' = 'fr'): boolean {
        const exists: boolean = SeriesRepo.isSeriesExist(userId, seriesId, lang);
        if (!exists) {
            throw new Error(lang === 'fr' ? 'Série non trouvée.' : 'Series not found.');
        }

        const deleted: boolean = SeriesRepo.removeBookFromSeries(seriesId, bookId, lang);
        if (deleted) {
            RemovedItem.deleteTracker(userId, null, 'series_books', `${seriesId}_${bookId}`, deletedAt, lang);
        }
        return deleted;
    }

    /**
     * Updates the order of books in a series.
     * @param userId - The unique identifier of the user
     * @param seriesId - The unique identifier of the series
     * @param booksOrder - An array of {bookId, order} objects
     * @param lang - The language for error messages ('fr' or 'en')
     * @returns True if the update was successful
     */
    public static updateBooksOrder(userId: string, seriesId: string, booksOrder: BooksOrderPost[], lang: 'fr' | 'en' = 'fr'): boolean {
        const exists: boolean = SeriesRepo.isSeriesExist(userId, seriesId, lang);
        if (!exists) {
            throw new Error(lang === 'fr' ? 'Série non trouvée.' : 'Series not found.');
        }

        return SeriesRepo.updateBooksOrder(seriesId, booksOrder, lang);
    }

    /**
     * Gets the series ID for a book if it belongs to one.
     * @param bookId - The unique identifier of the book
     * @returns The series ID or null
     */
    public static getSeriesIdForBook(bookId: string): string | null {
        return SeriesRepo.getSeriesIdForBook(bookId);
    }

    /**
     * Gets only the books of a series (without series details).
     * @param userId - The unique identifier of the user
     * @param seriesId - The unique identifier of the series
     * @param lang - The language for error messages ('fr' or 'en')
     * @returns The list of books in the series
     */
    public static getSeriesBooks(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesBookProps[] {
        const userKey: string = getUserEncryptionKey(userId);

        const exists: boolean = SeriesRepo.isSeriesExist(userId, seriesId, lang);
        if (!exists) {
            throw new Error(lang === 'fr' ? 'Série non trouvée.' : 'Series not found.');
        }

        const booksResult: SeriesBookResult[] = SeriesRepo.fetchSeriesBooks(userId, seriesId, lang);

        return booksResult.map((book: SeriesBookResult): SeriesBookProps => ({
            bookId: book.book_id,
            title: System.decryptDataWithUserKey(book.title, userKey),
            order: book.book_order,
            coverImage: book.cover_image
        }));
    }
}
