import SwiftUI
import Combine

public struct ClipboardNotice: Identifiable, Equatable {
    public let id = UUID()
    public let filename: String
}

@MainActor
public class MenuBarController: NSObject, ObservableObject, NSWindowDelegate {
    public static let minimumSearchCharacters = 3

    @Published public var config: SyncConfig
    @Published public var isSyncing = false
    @Published public var lastSyncTime: Date?
    @Published public var syncStatus: String = "Ready"
    @Published public var pendingChanges = 0
    @Published public var uploadProgress: Double? = nil
    @Published public private(set) var recentUploads: [AttachmentRecord] = []
    @Published public private(set) var isLoadingRecentUploads = false
    @Published public private(set) var recentUploadsError: String?
    @Published public private(set) var clipboardNotice: ClipboardNotice?
    @Published public var searchQuery = ""
    @Published public private(set) var searchResults: [SearchResult] = []
    @Published public private(set) var isSearching = false
    @Published public private(set) var searchError: String?
    @Published public var selectedSearchIndex = 0
    
    public var cancellables = Set<AnyCancellable>()
    
    private let configStore = ConfigStore.shared
    private var apiClient: APIClient
    private var syncEngine: SyncEngine?
    private var uploadManager: UploadManager
    private var folderWatcher: FolderWatcher?
    private var syncTimer: Timer?
    private var searchTask: Task<Void, Never>?
    
    private var settingsWindow: NSWindow?
    private var uploadsWindow: NSWindow?
    private var dropZoneWindow: NSWindow?
    
    public override init() {
        let config = ConfigStore.shared.load()
        self.config = config
        self.apiClient = APIClient(baseURL: config.serverBaseURL ?? URL(string: "http://localhost:8080")!, apiToken: config.apiToken)
        self.uploadManager = UploadManager(client: apiClient, config: config)
        self.syncEngine = SyncEngine(client: apiClient, config: config)
        super.init()
        
        Task {
            await setupSync()
        }
        Task {
            await refreshRecentUploads()
        }
    }
    
    public func updateConfig(_ newConfig: SyncConfig) {
        config = newConfig
        
        Task {
            await configStore.save(newConfig)
            
            if let url = newConfig.serverBaseURL {
                await apiClient.setAPIToken(newConfig.apiToken)
                apiClient = APIClient(baseURL: url, apiToken: newConfig.apiToken)
                uploadManager = UploadManager(client: apiClient, config: newConfig)
                syncEngine = SyncEngine(client: apiClient, config: newConfig)
                resetSearch()
                await refreshRecentUploads()
                await setupSync()
            }
        }
    }
    
    private func setupSync() async {
        // Stop existing watcher
        folderWatcher?.stop()
        syncTimer?.invalidate()
        
        guard config.autoSync, let syncFolder = config.syncFolder else {
            syncStatus = "Auto-sync disabled"
            return
        }
        
        // Setup folder watcher
        let watcher = FolderWatcher(folderPath: syncFolder)
        watcher.delegate = self
        watcher.start()
        self.folderWatcher = watcher
        
        // Setup periodic sync
        syncTimer = Timer.scheduledTimer(withTimeInterval: TimeInterval(config.syncInterval), repeats: true) { [weak self] _ in
            Task { [weak self] in
                await self?.performSync()
            }
        }
        
        // Initial sync
        await performSync()
    }
    
    public func performSync() async {
        guard !isSyncing else { return }
        guard config.syncFolder != nil else {
            syncStatus = "No sync folder configured"
            return
        }
        
        isSyncing = true
        syncStatus = "Syncing..."
        
        do {
            let engine = syncEngine ?? SyncEngine(client: apiClient, config: config)
            syncEngine = engine
            try await engine.performSync()
            
            lastSyncTime = Date()
            syncStatus = "Synced"
        } catch {
            syncStatus = "Sync failed: \(error.localizedDescription)"
        }
        
        isSyncing = false
    }
    
    // MARK: - Window Management
    
    public func showSettings() {
        if let window = settingsWindow {
            present(window)
            return
        }
        
        let window = NSWindow(
            contentRect: NSRect(x: 0, y: 0, width: 520, height: 640),
            styleMask: [.titled, .closable],
            backing: .buffered,
            defer: false
        )
        window.title = "Cairnquire Settings"
        window.titlebarAppearsTransparent = true
        window.isReleasedWhenClosed = false
        window.delegate = self
        window.contentView = NSHostingView(rootView: SettingsView(config: config, onSave: { [weak self, weak window] newConfig in
            self?.updateConfig(newConfig)
            window?.close()
        }))
        window.center()
        
        settingsWindow = window
        present(window)
    }
    
    public func showUploads() {
        if let window = uploadsWindow {
            present(window)
            return
        }
        
        let window = NSWindow(
            contentRect: NSRect(x: 0, y: 0, width: 400, height: 500),
            styleMask: [.titled, .closable, .miniaturizable, .resizable],
            backing: .buffered,
            defer: false
        )
        window.title = "Uploaded Files"
        window.isReleasedWhenClosed = false
        window.delegate = self
        window.contentView = NSHostingView(rootView: UploadsListView(client: apiClient))
        window.center()
        
        uploadsWindow = window
        present(window)
    }
    
    public func showDropZone() {
        if let window = dropZoneWindow {
            present(window)
            return
        }
        
        let window = NSWindow(
            contentRect: NSRect(x: 0, y: 0, width: 300, height: 250),
            styleMask: [.titled, .closable],
            backing: .buffered,
            defer: false
        )
        window.title = "Upload File"
        window.isReleasedWhenClosed = false
        window.delegate = self
        window.contentView = NSHostingView(rootView: DropZoneView(uploadManager: uploadManager))
        window.center()
        
        dropZoneWindow = window
        present(window)
    }

    public func windowWillClose(_ notification: Notification) {
        guard let window = notification.object as? NSWindow else { return }

        if window === settingsWindow {
            settingsWindow = nil
        } else if window === uploadsWindow {
            uploadsWindow = nil
        } else if window === dropZoneWindow {
            dropZoneWindow = nil
        }
    }

    private func present(_ window: NSWindow) {
        NSApp.activate(ignoringOtherApps: true)
        window.makeKeyAndOrderFront(nil)
    }
    
    public func quickUpload() {
        let panel = NSOpenPanel()
        panel.canChooseFiles = true
        panel.canChooseDirectories = false
        panel.allowsMultipleSelection = true
        
        guard panel.runModal() == .OK else { return }
        
        Task {
            await uploadURLList(panel.urls)
        }
    }
    
    public func uploadFiles(_ urls: [URL]) {
        Task {
            await uploadURLList(urls)
        }
    }

    private func uploadURLList(_ urls: [URL]) async {
        guard !urls.isEmpty else { return }

        let total = Double(urls.count)
        for (index, url) in urls.enumerated() {
            let baseProgress = Double(index) / total
            let fileWeight = 1 / total
            uploadProgress = baseProgress
            syncStatus = "Uploading \(url.lastPathComponent)"

            let onProgress: UploadProgressHandler = { [weak self] fileProgress in
                let combinedProgress = baseProgress + min(max(fileProgress, 0), 1) * fileWeight
                Task { @MainActor in
                    self?.uploadProgress = combinedProgress
                }
            }

            do {
                let result = try await uploadFileResolvingDuplicates(url, onProgress: onProgress)
                uploadProgress = Double(index + 1) / total
                syncStatus = "Uploaded \(result.filename)"
                clipboardNotice = ClipboardNotice(filename: result.filename)
                await refreshRecentUploads()
            } catch {
                syncStatus = "Upload failed: \(error.localizedDescription)"
            }
        }
        uploadProgress = nil
    }

    private func uploadFileResolvingDuplicates(
        _ url: URL,
        onProgress: UploadProgressHandler?
    ) async throws -> UploadResult {
        var duplicateAction: UploadDuplicateAction?

        while true {
            do {
                return try await uploadManager.uploadFile(
                    url,
                    duplicateAction: duplicateAction,
                    onProgress: onProgress
                )
            } catch let error as APIError where error.isUploadConflict {
                guard let action = promptForUploadDuplicate(error, filename: url.lastPathComponent) else {
                    throw UploadCancelledError()
                }
                duplicateAction = action
            }
        }
    }

    private func uploadDataResolvingDuplicates(_ data: Data, filename: String) async throws -> UploadResult {
        var duplicateAction: UploadDuplicateAction?

        while true {
            do {
                return try await uploadManager.uploadData(
                    data,
                    filename: filename,
                    duplicateAction: duplicateAction
                )
            } catch let error as APIError where error.isUploadConflict {
                guard let action = promptForUploadDuplicate(error, filename: filename) else {
                    throw UploadCancelledError()
                }
                duplicateAction = action
            }
        }
    }

    public func refreshRecentUploads() async {
        guard !isLoadingRecentUploads else { return }

        isLoadingRecentUploads = true
        defer { isLoadingRecentUploads = false }

        do {
            recentUploads = Array(try await apiClient.listAttachments().prefix(3))
            recentUploadsError = nil
        } catch {
            recentUploadsError = error.localizedDescription
        }
    }

    public func updateSearchQuery(_ query: String) {
        searchQuery = query
        selectedSearchIndex = 0
        searchTask?.cancel()

        let trimmed = query.trimmingCharacters(in: .whitespacesAndNewlines)
        guard trimmed.count >= Self.minimumSearchCharacters else {
            searchResults = []
            isSearching = false
            searchError = nil
            return
        }

        isSearching = true
        searchError = nil
        searchTask = Task { [weak self] in
            do {
                try await Task.sleep(for: .milliseconds(300))
                await self?.performSearch(for: trimmed)
            } catch {
                await MainActor.run {
                    if self?.isCurrentSearchQuery(trimmed) == true {
                        self?.isSearching = false
                    }
                }
            }
        }
    }

    public func clearSearch() {
        resetSearch()
    }

    public func moveSearchSelection(by delta: Int) {
        guard !searchResults.isEmpty else { return }
        selectedSearchIndex = min(max(selectedSearchIndex + delta, 0), searchResults.count - 1)
    }

    public func openSelectedSearchResult() {
        guard searchResults.indices.contains(selectedSearchIndex) else { return }
        openSearchResult(searchResults[selectedSearchIndex])
    }

    public func openSearchResult(_ result: SearchResult) {
        guard let url = documentURL(for: result) else { return }
        NSWorkspace.shared.open(url)
    }

    private func performSearch(for query: String) async {
        do {
            let results = try await apiClient.searchDocuments(query: liveSearchQuery(for: query))
            guard !Task.isCancelled else { return }
            guard isCurrentSearchQuery(query) else { return }
            searchResults = Array(results.prefix(8))
            selectedSearchIndex = min(selectedSearchIndex, max(searchResults.count - 1, 0))
            searchError = nil
        } catch {
            guard !Task.isCancelled else { return }
            guard isCurrentSearchQuery(query) else { return }
            searchResults = []
            searchError = error.localizedDescription
        }
        isSearching = false
    }

    private func isCurrentSearchQuery(_ query: String) -> Bool {
        let trimmed = searchQuery.trimmingCharacters(in: .whitespacesAndNewlines)
        return trimmed == query && trimmed.count >= Self.minimumSearchCharacters
    }

    private func resetSearch() {
        searchTask?.cancel()
        searchQuery = ""
        searchResults = []
        isSearching = false
        searchError = nil
        selectedSearchIndex = 0
    }

    private func liveSearchQuery(for query: String) -> String {
        let terms = query
            .components(separatedBy: CharacterSet.alphanumerics.inverted)
            .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
            .filter { !$0.isEmpty }

        guard !terms.isEmpty else { return query }
        return terms.map { "\($0)*" }.joined(separator: " ")
    }

    private func documentURL(for result: SearchResult) -> URL? {
        guard let baseURL = config.serverBaseURL else { return nil }
        let trimmedPath = result.path.hasSuffix(".md") ? String(result.path.dropLast(3)) : result.path
        var allowed = CharacterSet.urlPathAllowed
        allowed.remove(charactersIn: "?#")
        guard let encodedPath = trimmedPath.addingPercentEncoding(withAllowedCharacters: allowed) else {
            return nil
        }
        return URL(string: "docs/\(encodedPath)", relativeTo: baseURL)?.absoluteURL
    }
    
    public func createNote() {
        switch config.effectiveNewNoteAction {
        case .clipboard:
            createNoteFromClipboard()
        case .editor:
            createNoteInEditor()
        }
    }

    public func createNoteFromClipboard() {
        guard let text = NSPasteboard.general.string(forType: .string) else {
            syncStatus = "Clipboard is empty"
            return
        }
        
        Task {
            do {
                let result = try await uploadManager.createNote(from: text)
                syncStatus = "Created note: \(result.title)"
            } catch {
                syncStatus = "Failed to create note: \(error.localizedDescription)"
            }
        }
    }

    private func createNoteInEditor() {
        let draftURL = FileManager.default.temporaryDirectory
            .appendingPathComponent("cairnquire-\(UUID().uuidString)")
            .appendingPathExtension("md")
        let completionURL = draftURL.appendingPathExtension("done")

        do {
            try Data().write(to: draftURL, options: .atomic)
            try launchTerminalEditor(draftURL: draftURL, completionURL: completionURL)
            syncStatus = "Waiting for $EDITOR to exit"
        } catch {
            try? FileManager.default.removeItem(at: draftURL)
            syncStatus = "Failed to open $EDITOR: \(error.localizedDescription)"
            return
        }

        Task { [weak self] in
            await self?.uploadEditorDraft(draftURL: draftURL, completionURL: completionURL)
        }
    }

    private func uploadEditorDraft(draftURL: URL, completionURL: URL) async {
        defer {
            try? FileManager.default.removeItem(at: draftURL)
            try? FileManager.default.removeItem(at: completionURL)
        }

        while !FileManager.default.fileExists(atPath: completionURL.path) {
            try? await Task.sleep(for: .milliseconds(500))
        }

        do {
            let data = try Data(contentsOf: draftURL)
            guard !data.isEmpty else {
                syncStatus = "Editor note was empty; nothing uploaded"
                return
            }

            let filename = Self.editorNoteFilename()
            let result = try await uploadDataResolvingDuplicates(data, filename: filename)
            syncStatus = "Uploaded \(result.filename)"
            clipboardNotice = ClipboardNotice(filename: result.filename)
            await refreshRecentUploads()
        } catch {
            syncStatus = "Failed to upload editor note: \(error.localizedDescription)"
        }
    }

    private func launchTerminalEditor(draftURL: URL, completionURL: URL) throws {
        let draftPath = Self.shellQuote(draftURL.path)
        let completionPath = Self.shellQuote(completionURL.path)
        let shellCommand = "trap '/usr/bin/touch \(completionPath)' EXIT; editor=\"${EDITOR:-${VISUAL:-vi}}\"; eval \"$editor \(draftPath)\""
        let script = "tell application \"Terminal\" to do script \"\(Self.appleScriptEscape(shellCommand))\""

        let process = Process()
        let errorPipe = Pipe()
        process.executableURL = URL(fileURLWithPath: "/usr/bin/osascript")
        process.arguments = ["-e", "tell application \"Terminal\" to activate", "-e", script]
        process.standardError = errorPipe
        try process.run()
        process.waitUntilExit()

        guard process.terminationStatus == 0 else {
            let data = errorPipe.fileHandleForReading.readDataToEndOfFile()
            let message = String(data: data, encoding: .utf8)?
                .trimmingCharacters(in: .whitespacesAndNewlines)
            throw NSError(
                domain: "CairnquireSync",
                code: Int(process.terminationStatus),
                userInfo: [NSLocalizedDescriptionKey: message ?? "Terminal could not be opened"]
            )
        }
    }

    private static func shellQuote(_ value: String) -> String {
        "'" + value.replacingOccurrences(of: "'", with: "'\\''") + "'"
    }

    private static func appleScriptEscape(_ value: String) -> String {
        value
            .replacingOccurrences(of: "\\", with: "\\\\")
            .replacingOccurrences(of: "\"", with: "\\\"")
    }

    private static func editorNoteFilename() -> String {
        let formatter = DateFormatter()
        formatter.locale = Locale(identifier: "en_US_POSIX")
        formatter.dateFormat = "yyyy-MM-dd HH-mm-ss"
        return "Note \(formatter.string(from: Date())).md"
    }
}

extension MenuBarController: FolderWatcherDelegate {
    public nonisolated func folderWatcher(_ watcher: FolderWatcher, didDetectChanges paths: [String]) {
        Task { @MainActor in
            pendingChanges += paths.count
            syncStatus = "\(pendingChanges) pending changes"
            await performSync()
            pendingChanges = 0
        }
    }
}
