// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import {
    PDFDocument,
    PDFAnnotation,
    PDFBookmark,
    UserPresence,
} from "./pdfTypes.js";
import registerDebug from "debug";
import * as fs from "fs/promises";
import * as path from "path";
import * as os from "os";
import sanitizeFilename from "sanitize-filename";
import xss from "xss";

const debug = registerDebug("typeagent:views:server:pdf:service");

function sanitizeBookmark(bookmark: PDFBookmark): PDFBookmark {
    // Sanitize string fields to mitigate XSS when bookmarks are rendered on the client
    const sanitized: PDFBookmark = { ...bookmark };

    for (const key of Object.keys(sanitized) as (keyof PDFBookmark)[]) {
        const value = sanitized[key];
        if (typeof value === "string") {
            // Use xss to strip/escape any potentially dangerous HTML/JS
            (sanitized as any)[key] = xss(value) as PDFBookmark[typeof key];
        }
    }

    return sanitized;
}

// Recursively sanitize all string properties using the xss library.
function sanitizeValue<T>(value: T): T {
    if (typeof value === "string") {
        return xss(value) as unknown as T;
    }

    if (Array.isArray(value)) {
        return value.map((item) => sanitizeValue(item)) as unknown as T;
    }

    if (value && typeof value === "object") {
        const result: Record<string, unknown> = {};
        for (const [key, v] of Object.entries(
            value as Record<string, unknown>,
        )) {
            result[key] = sanitizeValue(v);
        }
        return result as T;
    }

    return value;
}

function sanitizeAnnotation(annotation: PDFAnnotation): PDFAnnotation {
    return sanitizeValue(annotation);
}

/**
 * PDF business logic service
 */
export class PDFService {
    private documents: Map<string, PDFDocument> = new Map();
    private annotations: Map<string, PDFAnnotation[]> = new Map();
    private bookmarks: Map<string, PDFBookmark[]> = new Map();
    private userPresence: Map<string, Map<string, UserPresence>> = new Map();
    private storePath: string = "";
    private annotationsPath: string = "";

    constructor() {
        this.initializeStoragePaths();
    }

    /**
     * Initialize storage paths and load existing data
     */
    private initializeStoragePaths(): void {
        this.storePath =
            process.env.TYPEAGENT_BROWSER_FILES ||
            path.join(os.homedir(), ".typeagent", "browser", "viewstore");

        this.annotationsPath = path.join(this.storePath, "annotations");
        debug(`PDF storage initialized at: ${this.storePath}`);
    }

    /**
     * Initialize the service with storage and sample data
     */
    async initialize(): Promise<void> {
        try {
            // Ensure storage directory exists
            await fs.mkdir(this.annotationsPath, { recursive: true });
            debug(
                `Annotations directory created/verified: ${this.annotationsPath}`,
            );

            // Load existing annotations from disk
            await this.loadAnnotationsFromDisk();

            // Initialize sample data (only if no existing data)
            this.initializeSampleData();

            debug("PDF service initialization complete");
        } catch (error) {
            debug("Error initializing PDF service:", error);
            // Fallback to in-memory only
            this.initializeSampleData();
        }
    }

    /**
     * Load annotations from disk storage
     */
    private async loadAnnotationsFromDisk(): Promise<void> {
        try {
            const files = await fs.readdir(this.annotationsPath);
            const annotationFiles = files.filter((file) =>
                file.endsWith(".json"),
            );

            for (const file of annotationFiles) {
                const documentId = path.basename(file, ".json");
                const filePath = path.join(this.annotationsPath, file);

                try {
                    const data = await fs.readFile(filePath, "utf-8");
                    const annotations: PDFAnnotation[] = JSON.parse(data);
                    this.annotations.set(documentId, annotations);
                    debug(
                        `Loaded ${annotations.length} annotations for document ${documentId}`,
                    );
                } catch (parseError) {
                    debug(
                        `Error parsing annotations file ${file}:`,
                        parseError,
                    );
                }
            }

            debug(
                `Loaded annotations for ${annotationFiles.length} documents from disk`,
            );
        } catch (error) {
            debug("Error loading annotations from disk:", error);
            // Continue with empty annotations if loading fails
        }
    }

    /**
     * Save annotations for a document to disk
     */
    private async saveAnnotationsToDisk(documentId: string): Promise<void> {
        try {
            const annotations = this.annotations.get(documentId) || [];
            const sanitizedDocumentId =
                sanitizeFilename(documentId) || "default";
            const filePath = path.resolve(
                this.annotationsPath,
                `${sanitizedDocumentId}.json`,
            );

            if (!filePath.startsWith(this.annotationsPath)) {
                throw new Error("Invalid documentId: Path traversal detected");
            }

            await fs.writeFile(
                filePath,
                JSON.stringify(annotations, null, 2),
                "utf-8",
            );
            debug(
                `Saved ${annotations.length} annotations for document ${documentId} to disk`,
            );
        } catch (error) {
            debug(
                `Error saving annotations for document ${documentId}:`,
                error,
            );
            // Don't throw - continue operation even if disk write fails
        }
    }

    /**
     * Initialize with sample data for development
     */
    private initializeSampleData(): void {
        // Add a sample PDF document
        const sampleDoc: PDFDocument = {
            id: "sample-pdf-1",
            title: "Sample PDF Document",
            filename: "sample.pdf",
            size: 1024 * 1024, // 1MB
            pageCount: 10,
            uploadDate: new Date().toISOString(),
            lastModified: new Date().toISOString(),
            mimeType: "application/pdf",
        };

        this.documents.set(sampleDoc.id, sampleDoc);
        this.annotations.set(sampleDoc.id, []);
        this.bookmarks.set(sampleDoc.id, []);
        this.userPresence.set(sampleDoc.id, new Map());

        debug("Sample PDF data initialized");
    }

    /**
     * Get PDF document by ID
     */
    getDocument(documentId: string): PDFDocument | null {
        return this.documents.get(documentId) || null;
    }

    /**
     * Get all documents
     */
    getAllDocuments(): PDFDocument[] {
        return Array.from(this.documents.values());
    }

    /**
     * Add new PDF document
     */
    addDocument(document: PDFDocument): void {
        this.documents.set(document.id, document);

        // Only initialize annotations if they don't already exist (preserve loaded annotations)
        if (!this.annotations.has(document.id)) {
            this.annotations.set(document.id, []);
        }

        // Only initialize bookmarks if they don't already exist
        if (!this.bookmarks.has(document.id)) {
            this.bookmarks.set(document.id, []);
        }

        // Only initialize presence if it doesn't already exist
        if (!this.userPresence.has(document.id)) {
            this.userPresence.set(document.id, new Map());
        }

        debug(`Added new document: ${document.id}`);
    }

    /**
     * Get annotations for a document
     */
    async getAnnotations(documentId: string): Promise<PDFAnnotation[]> {
        // First try to get from memory
        let annotations = this.annotations.get(documentId);

        // If not in memory, try to load from disk
        if (!annotations) {
            try {
                const sanitizedDocumentId =
                    sanitizeFilename(documentId) || "default";
                const filePath = path.resolve(
                    this.annotationsPath,
                    `${sanitizedDocumentId}.json`,
                );
                if (!filePath.startsWith(this.annotationsPath)) {
                    throw new Error(
                        "Invalid documentId: Path traversal detected",
                    );
                }

                const data = await fs.readFile(filePath, "utf-8");
                const loadedAnnotations: PDFAnnotation[] = JSON.parse(data);

                // Cache in memory for future requests
                this.annotations.set(documentId, loadedAnnotations);
                annotations = loadedAnnotations;
                debug(
                    `Loaded ${loadedAnnotations.length} annotations for document ${documentId} from disk on demand`,
                );
            } catch (error) {
                // File doesn't exist or can't be read, return empty array
                annotations = [];
                this.annotations.set(documentId, annotations);
                debug(
                    `No annotations found for document ${documentId}, starting with empty array`,
                );
            }
        }

        return annotations;
    }

    /**
     * Add annotation to a document
     */
    async addAnnotation(annotation: PDFAnnotation): Promise<PDFAnnotation> {
        const docAnnotations =
            this.annotations.get(annotation.documentId) || [];
        const sanitizedAnnotation = sanitizeAnnotation(annotation);
        docAnnotations.push(sanitizedAnnotation);
        this.annotations.set(sanitizedAnnotation.documentId, docAnnotations);

        // Persist to disk
        await this.saveAnnotationsToDisk(sanitizedAnnotation.documentId);

        debug(
            `Added annotation ${sanitizedAnnotation.id} to document ${sanitizedAnnotation.documentId}`,
        );
        return sanitizedAnnotation;
    }

    /**
     * Update annotation
     */
    async updateAnnotation(
        annotation: PDFAnnotation,
    ): Promise<PDFAnnotation | null> {
        const docAnnotations =
            this.annotations.get(annotation.documentId) || [];
        const index = docAnnotations.findIndex((a) => a.id === annotation.id);

        if (index !== -1) {
            const sanitizedAnnotation = sanitizeAnnotation({
                ...annotation,
                updatedAt: new Date().toISOString(),
            });
            docAnnotations[index] = sanitizedAnnotation;

            // Persist to disk
            await this.saveAnnotationsToDisk(sanitizedAnnotation.documentId);

            debug(`Updated annotation ${sanitizedAnnotation.id}`);
            return sanitizedAnnotation;
        }

        return null;
    }

    /**
     * Delete annotation
     */
    async deleteAnnotation(
        documentId: string,
        annotationId: string,
    ): Promise<boolean> {
        const docAnnotations = this.annotations.get(documentId) || [];
        const index = docAnnotations.findIndex((a) => a.id === annotationId);

        if (index !== -1) {
            docAnnotations.splice(index, 1);

            // Persist to disk
            await this.saveAnnotationsToDisk(documentId);

            debug(`Deleted annotation ${annotationId}`);
            return true;
        }

        return false;
    }
    /**
     * Get bookmarks for a document
     */
    getBookmarks(documentId: string): PDFBookmark[] {
        const docBookmarks = this.bookmarks.get(documentId) || [];
        // Return sanitized copies to ensure any previously stored data is safe
        return docBookmarks.map((bookmark) => sanitizeBookmark(bookmark));
    }

    /**
     * Add bookmark to a document
     */
    addBookmark(bookmark: PDFBookmark): PDFBookmark {
        const sanitizedBookmark = sanitizeBookmark(bookmark);
        const docBookmarks =
            this.bookmarks.get(sanitizedBookmark.documentId) || [];
        docBookmarks.push(sanitizedBookmark);
        this.bookmarks.set(sanitizedBookmark.documentId, docBookmarks);

        debug(
            `Added bookmark ${sanitizedBookmark.id} to document ${sanitizedBookmark.documentId}`,
        );
        return sanitizedBookmark;
    }

    /**
     * Delete bookmark
     */
    deleteBookmark(documentId: string, bookmarkId: string): boolean {
        const docBookmarks = this.bookmarks.get(documentId) || [];
        const index = docBookmarks.findIndex((b) => b.id === bookmarkId);

        if (index !== -1) {
            docBookmarks.splice(index, 1);
            debug(`Deleted bookmark ${bookmarkId}`);
            return true;
        }

        return false;
    }

    /**
     * Update user presence for a document
     */
    updateUserPresence(documentId: string, presence: UserPresence): void {
        const docPresence = this.userPresence.get(documentId) || new Map();
        docPresence.set(presence.userId, presence);
        this.userPresence.set(documentId, docPresence);

        debug(
            `Updated presence for user ${presence.userId} in document ${documentId}`,
        );
    }

    /**
     * Remove user presence
     */
    removeUserPresence(documentId: string, userId: string): void {
        const docPresence = this.userPresence.get(documentId);
        if (docPresence) {
            docPresence.delete(userId);
            debug(
                `Removed presence for user ${userId} from document ${documentId}`,
            );
        }
    }

    /**
     * Get all users present in a document
     */
    getUserPresence(documentId: string): UserPresence[] {
        const docPresence = this.userPresence.get(documentId);
        return docPresence ? Array.from(docPresence.values()) : [];
    }

    /**
     * Get all annotations across all documents (reads from storage)
     */
    async getAllAnnotations(): Promise<
        Array<PDFAnnotation & { documentTitle: string }>
    > {
        const allAnnotations: Array<PDFAnnotation & { documentTitle: string }> =
            [];

        try {
            // Read all annotation files from disk
            const files = await fs.readdir(this.annotationsPath);
            const annotationFiles = files.filter((file) =>
                file.endsWith(".json"),
            );

            for (const file of annotationFiles) {
                const documentId = path.basename(file, ".json");
                const filePath = path.join(this.annotationsPath, file);

                try {
                    const data = await fs.readFile(filePath, "utf-8");
                    const annotations: PDFAnnotation[] = JSON.parse(data);

                    // Get document title (try from memory first, then create meaningful fallback)
                    const document = this.documents.get(documentId);
                    let documentTitle = document?.title;

                    if (!documentTitle) {
                        // Try to extract a meaningful title from the documentId
                        if (
                            documentId.includes("/") &&
                            documentId.includes(".")
                        ) {
                            // Looks like a URL path, extract filename
                            const parts = documentId.split("/");
                            const filename = parts[parts.length - 1];
                            documentTitle = filename.replace(
                                /\.(pdf|PDF)$/,
                                "",
                            );
                        } else {
                            // Use documentId but make it more readable
                            documentTitle = `Document ${documentId}`;
                        }
                    }

                    // Add enriched annotations
                    const enrichedAnnotations = annotations.map(
                        (annotation) => ({
                            ...annotation,
                            documentTitle,
                        }),
                    );

                    allAnnotations.push(...enrichedAnnotations);

                    debug(
                        `Loaded ${annotations.length} annotations from ${file} (${documentTitle})`,
                    );
                } catch (parseError) {
                    debug(
                        `Error parsing annotations file ${file}:`,
                        parseError,
                    );
                }
            }

            debug(
                `Loaded total ${allAnnotations.length} annotations from ${annotationFiles.length} documents`,
            );
        } catch (error) {
            debug("Error reading annotations from storage:", error);
            // Continue with empty array if storage read fails
        }

        // Sort by creation date (newest first)
        return allAnnotations.sort(
            (a, b) =>
                new Date(b.createdAt).getTime() -
                new Date(a.createdAt).getTime(),
        );
    }

    /**
     * Search annotations by content
     */
    async searchAnnotations(
        query: string,
    ): Promise<Array<PDFAnnotation & { documentTitle: string }>> {
        const allAnnotations = await this.getAllAnnotations();
        const lowerQuery = query.toLowerCase();

        return allAnnotations.filter((annotation) => {
            return (
                annotation.content?.toLowerCase().includes(lowerQuery) ||
                annotation.metadata?.blockquoteContent
                    ?.toLowerCase()
                    .includes(lowerQuery) ||
                annotation.documentTitle.toLowerCase().includes(lowerQuery)
            );
        });
    }

    /**
     * Generate unique ID
     */
    generateId(): string {
        return `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
    }
}
