//
//  v_searchApp.swift
//  v-search
//
//  Created by Du Qingjie on 2024/09/07.
//

import SwiftUI
import PDFKit
import Foundation
import Compression
import ZIPFoundation

@main
struct v_searchApp: App {
    @StateObject var viewModel = AppViewModel()
    
    // Store the timer
    @State private var scanTimer: Timer? = nil
    @State private var taskRunning: Bool = false
    @State private var statusItem: NSStatusItem? // Tray icon status item
    
    @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

    var body: some Scene {
        WindowGroup {
            ContentView(viewModel: viewModel)
                .onAppear {
                    startBackgroundScan()
                }
                .onDisappear {
                    //stopBackgroundScan()
                }
        }
    }
    
    // Function to start the background scan with a timer
    func startBackgroundScan() {
        viewModel.statusMessage = NSLocalizedString("status_loading", comment: "")
        appDelegate.viewModel = self.viewModel
        //scanSavedFolders()
        
        // Schedule the timer to scan every 3 minutes (adjust interval as necessary)
        scanTimer = Timer.scheduledTimer(withTimeInterval: 10, repeats: true) { _ in
            if !taskRunning {
                taskRunning = true
                DispatchQueue.global(qos: .background).async {
                    scanSavedFolders()
                    DispatchQueue.main.async {
                        taskRunning = false
                    }
                }
            }
        }
        Logger.shared.debug("Background scan started.")
    }
    
    // Function to stop the background scan when the app closes
    func stopBackgroundScan() {
        scanTimer?.invalidate()
        Logger.shared.debug("Background scan stopped.")
    }

    // Function to scan the saved folders and add files to the VectorIndex
    func scanSavedFolders() {
        guard let folders = UserDefaults.standard.stringArray(forKey: "savedFolders") else {
            Logger.shared.debug("No folders found in config.")
            return
        }

        viewModel.statusMessage = NSLocalizedString("scan_start", comment: "") 
        for folder in folders {
            let semaphore = DispatchSemaphore(value: 0)
            Logger.shared.debug("Scanning folder: \(folder)")
            viewModel.statusMessage = String.localizedStringWithFormat(NSLocalizedString("scanning_folder", comment: ""), folder)
            Task {
                do {
                    if let bookmarkData = UserDefaults.standard.data(forKey: folder) {
                        //Logger.shared.debug("Got bookmarkData")
                        var isStale = false
                        let restoredURL = try URL(resolvingBookmarkData: bookmarkData, options: [.withSecurityScope], relativeTo: nil, bookmarkDataIsStale: &isStale)
                        if restoredURL.startAccessingSecurityScopedResource() {
                            //Logger.shared.debug("Start scanDirectory")
                            await scanDirectory(path: restoredURL.path)
                            //Logger.shared.debug("Done scanDirectory")
                            restoredURL.stopAccessingSecurityScopedResource()
                        }
                    }
                } catch {
                    Logger.shared.err("Error resolving bookmark: \(error)")
                }
                //Logger.shared.debug("Calling signal")
                semaphore.signal() // Signal that the async task is finished
                Logger.shared.debug("Called signal")
            }

            Logger.shared.debug("Waiting signal")
            // Wait for the async task to complete
            semaphore.wait()
            Logger.shared.debug("Done waiting")
            
            DispatchQueue.main.async {
                viewModel.statusMessage = String.localizedStringWithFormat(NSLocalizedString("scanned_folder", comment: ""), folder)
                Logger.shared.debug("Scan completed.")
            }
        }
    }

    enum FileType {
        case pdf, docx, pptx, unknown
    }
    
    // Recursively scan a directory and its subdirectories for text files
    func scanDirectory(path: String) async {
        let fileManager = FileManager.default
        do {
            let contents = try fileManager.contentsOfDirectory(atPath: path)
            var count = 0
            for item in contents {
                let itemPath = "\(path)/\(item)"
                var isDir: ObjCBool = false
                if fileManager.fileExists(atPath: itemPath, isDirectory: &isDir) {
                    if isDir.boolValue {
                        sleep(1)
                        await scanDirectory(path: itemPath)
                    } else {
                        if count > 100 {
                            count = 0
                            sleep(1)
                        }
                        await processFile(path: itemPath)
                    }
                }
            }
        } catch {
            Logger.shared.err("Error scanning directory: \(error.localizedDescription)")
        }
    }
    
    func detectFileType(fileURL: URL) -> FileType? {
        guard let fileData = try? Data(contentsOf: fileURL) else { return nil }

        if fileData.prefix(4) == Data([0x25, 0x50, 0x44, 0x46]) {
            return .pdf
        }
        
        if fileData.prefix(4) == Data([0x50, 0x4B, 0x03, 0x04]) {
            if checkZipContainsFile(fileURL: fileURL, fileName: "[Content_Types].xml") &&
                    checkZipContainsFile(fileURL: fileURL, fileName:"word/document.xml") {
                return .docx
            }
        }
        
        if fileData.prefix(4) == Data([0x50, 0x4B, 0x03, 0x04]) {
            if checkZipContainsFile(fileURL: fileURL, fileName: "[Content_Types].xml") {
                return .pptx
            }
        }
        
        return .unknown
    }
    
    func getFileModificationTime(atPath path: String) -> Int? {
        let fileManager = FileManager.default
        do {
            let attributes = try fileManager.attributesOfItem(atPath: path)
            if let modificationDate = attributes[.modificationDate] as? Date {
                // Convert to Unix time (TimeInterval is in seconds since 1970)
                let unixTime = modificationDate.timeIntervalSince1970
                return Int(unixTime)
            }
        } catch {
            Logger.shared.err("Error retrieving file attributes: \(error.localizedDescription)")
        }
        return Int.max
    }

    // Function to process a file and add it to the VectorIndex
    func processFile(path: String) async {
        do {
            if let fileType = detectFileType(fileURL: URL(fileURLWithPath: path)) {
                var fileContents: String = ""
                switch fileType {
                case .pdf:
                    fileContents = extractTextFromPDF(fileURL: URL(fileURLWithPath: path)) ?? ""
                case .docx:
                    fileContents = extractTextFromDocx(fileURL: URL(fileURLWithPath: path)) ?? ""
                case .pptx:
                    fileContents = extractTextFromPPTX(fileURL: URL(fileURLWithPath: path)) ?? ""
                default:
                    fileContents = try String(contentsOfFile: path, encoding: .utf8)
                }
                if !fileContents.isEmpty {
                    await VectorManager.shared.setFileItem(path: path, text: String(fileContents.prefix(1000)),modify_time: getFileModificationTime(atPath: path) ?? Int.max)
                    Logger.shared.debug("File added to VectorIndex: \(path)")
                }
            }
        } catch {
            Logger.shared.debug("Error reading file at path: \(path) - \(error.localizedDescription)")
        }
    }
    
    func checkZipContainsFile(fileURL: URL, fileName: String) -> Bool {
        do {
            // Use the new throwing initializer to open the ZIP file
            let archive = try Archive(url: fileURL, accessMode: .read)
            
            // Iterate through the entries in the ZIP archive
            for entry in archive {
                if entry.path == fileName {
                    Logger.shared.debug("\(fileName) found in ZIP archive.")
                    return true
                }
            }
            
            Logger.shared.debug("\(fileName) not found in ZIP archive.")
            return false
        } catch {
            // Handle any errors that occur while trying to open the ZIP archive
            Logger.shared.err("Failed to open ZIP archive: \(error.localizedDescription)")
            return false
        }
    }
    
    func extractTextFromPDF(fileURL: URL) -> String? {
        guard let pdfDocument = PDFDocument(url: fileURL) else { return nil }
        var extractedText = ""
        for i in 0..<pdfDocument.pageCount {
            if let page = pdfDocument.page(at: i), let pageText = page.string {
                extractedText += pageText
            }
        }
        return extractedText
    }
    
    func extractTextFromDocx(fileURL: URL) -> String? {
        do {
            // Open the DOCX file as a ZIP archive
            let archive = try Archive(url: fileURL, accessMode: .read)
            
            // Look for the 'word/document.xml' file in the archive
            if let documentEntry = archive["word/document.xml"] {
                // Extract the XML data from the entry
                var documentXML = Data()
                _ = try archive.extract(documentEntry) { data in
                    documentXML.append(data)
                }
                
                // Convert XML data to string
                if let documentString = String(data: documentXML, encoding: .utf8) {
                    // Parse the XML and extract text content
                    return extractTextFromXML(xmlString: documentString)
                }
            }
        } catch {
            Logger.shared.err("Error extracting DOCX file: \(error.localizedDescription)")
        }
        
        return nil
    }
    
    func extractTextFromXML(xmlString: String) -> String {
        var extractedText = ""
        
        do {
            // Parse the XML
            let xmlDoc = try XMLDocument(xmlString: xmlString, options: .documentTidyXML)
            
            // Extract all text nodes from <w:t> (Word text) elements
            let textNodes = try xmlDoc.nodes(forXPath: "//w:t")
            for node in textNodes {
                if let textNode = node as? XMLElement {
                    extractedText += textNode.stringValue ?? ""
                }
            }
        } catch {
            Logger.shared.err("Error parsing XML: \(error.localizedDescription)")
        }
        
        return extractedText
    }

    func extractTextFromPPTX(fileURL: URL) -> String? {
        do {
            // Open the PPTX file as a ZIP archive
            let archive = try Archive(url: fileURL, accessMode: .read)
            var extractedText = ""

            // Look for all slide XML files (usually stored as ppt/slides/slide1.xml, slide2.xml, etc.)
            for entry in archive {
                if entry.path.hasPrefix("ppt/slides/slide") && entry.path.hasSuffix(".xml") {
                    // Extract the XML data from each slide
                    var slideXML = Data()
                    _ = try archive.extract(entry) { data in
                        slideXML.append(data)
                    }
                    
                    // Convert the slide XML data to string
                    if let slideString = String(data: slideXML, encoding: .utf8) {
                        // Parse the slide XML and extract text content
                        extractedText += extractTextFromPPTXSlide(xmlString: slideString) + "\n"
                    }
                }
            }
            
            return extractedText.isEmpty ? nil : extractedText
        } catch {
            Logger.shared.err("Error extracting PPTX file: \(error.localizedDescription)")
        }
        
        return nil
    }

    // Helper function to extract text content from a slide's XML
    func extractTextFromPPTXSlide(xmlString: String) -> String {
        var extractedText = ""
        
        do {
            // Parse the XML
            let xmlDoc = try XMLDocument(xmlString: xmlString, options: .documentTidyXML)
            
            // Extract all text nodes from <a:t> (PowerPoint text) elements
            let textNodes = try xmlDoc.nodes(forXPath: "//a:t")
            for node in textNodes {
                if let textNode = node as? XMLElement {
                    extractedText += textNode.stringValue ?? ""
                }
            }
        } catch {
            Logger.shared.err("Error parsing slide XML: \(error.localizedDescription)")
        }
        
        return extractedText
    }

}

class AppDelegate: NSObject, NSApplicationDelegate {
    var statusItem: NSStatusItem!
    var mainWindow: NSWindow?

    public var viewModel: AppViewModel?
    
    func applicationDidFinishLaunching(_ notification: Notification) {
        // Initialize the status item (tray icon)
        statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
        
        if let button = statusItem.button {
            button.image = NSImage(named: "TrayIcon")//, accessibilityDescription: "Tray Icon")
            button.action = #selector(toggleWindow)
        }
        
        // Find the main window
        if let window = NSApplication.shared.windows.first {
            mainWindow = window
        }
        
        Task {
            await VectorManager.shared.load()
        }

        viewModel?.statusMessage = "Ready"
    }

    @objc func toggleWindow() {
        guard let window = mainWindow else { return }
        
        if window.isVisible {
            window.orderOut(nil) // Hide the window
            //Hide the icon on dock
            NSApp.setActivationPolicy(.accessory)
        } else {
            window.makeKeyAndOrderFront(nil) // Show the window
            //Show the icon on dock
            NSApp.setActivationPolicy(.regular)
        }
    }
}
