import Cocoa

@main
class AppDelegate: NSObject, NSApplicationDelegate {
    static func main() {
        let app = NSApplication.shared
        let delegate = AppDelegate()
        app.delegate = delegate
        app.setActivationPolicy(.accessory)
        app.run()
    }

    // MARK: - Varaibles

    var statusItem: NSStatusItem!
    var statusMenuItem: NSMenuItem!
    var outdatedItem: NSMenuItem!
    var upgradeAllItem: NSMenuItem!
    var outdatedSubmenu: NSMenu!
    var versionItem: NSMenuItem!

    var logWindow: LogWindowController?
    var settingsWindow: SettingsWindowController?
    var logBuffer: String = ""

    var brewPath: String?

    var refreshTimer: Timer?
    /// var refreshInterval: TimeInterval = 3600
    var refreshInterval: TimeInterval {
        get {
            let saved = UserDefaults.standard.double(forKey: "refreshInterval")
            return saved > 0 ? saved : 3600 // fallback 1h si jamais défini
        }
        set {
            UserDefaults.standard.set(newValue, forKey: "refreshInterval")
            UserDefaults.standard.synchronize()
        }
    }

    /// last known outdated list, shown in the Upgrade All confirmation
    var cachedOutdated: [String] = []

    // MARK: - Menu

    func applicationDidFinishLaunching(_: Notification) {
        Settings.registerDefaults()

        statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
        setMenuBarIcon("brewbar-uptodate")

        let menu = NSMenu()

        statusMenuItem = NSMenuItem(title: "Checking...", action: nil, keyEquivalent: "")
        menu.addItem(statusMenuItem)

        menu.addItem(NSMenuItem.separator())

        menu.addItem(NSMenuItem(title: "🔄 Refresh", action: #selector(refreshAction), keyEquivalent: "r"))
        upgradeAllItem = NSMenuItem(title: "📦 Upgrade All", action: #selector(upgradeAll), keyEquivalent: "u")
        upgradeAllItem.isHidden = true // shown once a fetch finds outdated packages
        menu.addItem(upgradeAllItem)
        outdatedItem = NSMenuItem(title: "📋 Outdated", action: nil, keyEquivalent: "")
        outdatedSubmenu = NSMenu()
        outdatedItem.submenu = outdatedSubmenu
        outdatedItem.isHidden = true // shown once a fetch finds outdated packages
        menu.addItem(outdatedItem)
        menu.addItem(NSMenuItem(title: "📜 Logs", action: #selector(showLogs), keyEquivalent: "l"))

        menu.addItem(NSMenuItem.separator())

        menu.addItem(NSMenuItem(title: "⚙️ Settings…", action: #selector(showSettings), keyEquivalent: ","))

        versionItem = NSMenuItem(title: "🏷 Brew: ...", action: nil, keyEquivalent: "")
        menu.addItem(versionItem)

        menu.addItem(NSMenuItem.separator())

        menu.addItem(NSMenuItem(title: "Quit", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q"))

        statusItem.menu = menu

        refreshAll()
        refreshBrewVersion()

        refreshTimer = Timer.scheduledTimer(withTimeInterval: refreshInterval, repeats: true) { _ in
            self.refreshAll()
        } // pour arrêter : refreshTimer?.invalidate()
    }

    // MARK: - Env func

    func setMenuBarIcon(_ name: String) {
        DispatchQueue.main.async {
            if let button = self.statusItem.button {
                if let icon = NSImage(named: name) {
                    icon.size = NSSize(width: 22, height: 22)
                    icon.isTemplate = true
                    button.image = icon
                }
            }
        }
    }

    func restartTimer() {
        refreshTimer?.invalidate() // stop running timer
        refreshTimer = Timer.scheduledTimer(withTimeInterval: refreshInterval, repeats: true) { _ in
            self.refreshAll()
        }
    }

    @objc func showSettings() {
        if settingsWindow == nil {
            settingsWindow = SettingsWindowController(appDelegate: self)
        }
        settingsWindow?.syncUI()
        settingsWindow?.showWindow(nil)
        // accessory app: without activation the window can open behind others
        NSApp.activate(ignoringOtherApps: true)
        settingsWindow?.window?.makeKeyAndOrderFront(nil)
    }

    func resolveBrewPath() -> String {
        if let cached = brewPath {
            return cached
        }

        let paths = [
            "/opt/homebrew/bin/brew",
            "/usr/local/bin/brew",
        ]

        for path in paths {
            if FileManager.default.isExecutableFile(atPath: path) {
                brewPath = path
                return path
            }
        }

        // fallback
        brewPath = "brew"
        return "brew"
    }

    /// Written once per launch; sudo runs it to show a GUI password dialog
    /// whenever brew needs admin rights (no terminal is attached to Process).
    static let askpassURL: URL = {
        let dir = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
            .appendingPathComponent("BrewBar", isDirectory: true)
        try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)

        let url = dir.appendingPathComponent("askpass.sh")
        let script = """
        #!/bin/zsh
        osascript -e 'display dialog "BrewBar needs your administrator password to continue:" default answer "" with hidden answer with title "BrewBar" with icon caution buttons {"Cancel", "OK"} default button "OK"' -e 'text returned of result'
        """
        try? script.write(to: url, atomically: true, encoding: .utf8)
        try? FileManager.default.setAttributes([.posixPermissions: 0o700], ofItemAtPath: url.path)
        return url
    }()

    func runBrew(_ command: String, completion: @escaping (String) -> Void = { _ in }) {
        DispatchQueue.global().async {
            let brew = self.resolveBrewPath()
            let cleaned = command.replacingOccurrences(of: "brew ", with: "")
            let arguments = [brew] + cleaned.split(separator: " ").map(String.init)

            self.log("→ \(arguments.joined(separator: " "))\n")

            // env resolves brew via PATH (covers the bare "brew" fallback) and
            // execs it directly — no shell, so no metacharacter interpretation
            let process = Process()
            process.executableURL = URL(fileURLWithPath: "/usr/bin/env")
            process.arguments = arguments

            var environment = ProcessInfo.processInfo.environment
            environment["SUDO_ASKPASS"] = Self.askpassURL.path
            process.environment = environment

            let pipe = Pipe()
            process.standardOutput = pipe
            process.standardError = pipe

            do {
                try process.run()
            } catch {
                self.log("ERROR: \(error)\n")
                DispatchQueue.main.async { completion("") }
                return
            }

            let data = pipe.fileHandleForReading.readDataToEndOfFile()
            process.waitUntilExit()

            let output = String(data: data, encoding: .utf8) ?? ""

            self.log(output)
            // completion always on main: callers update AppKit UI, which is main-thread only
            DispatchQueue.main.async { completion(output) }
        }
    }

    // MARK: - Actions

    @objc func refreshAction() {
        refreshAll()
    }

    @objc func upgradeAll() {
        guard confirmUpgrade() else { return }

        statusMenuItem.title = "Upgrading..."

        runBrew("update") { _ in
            self.runBrew("upgrade") { _ in
                // everything was just upgraded; don't chain an auto-upgrade
                self.fetchOutdated(allowAutoUpgrade: false)
            }
        }
    }

    func confirmUpgrade() -> Bool {
        let alert = NSAlert()
        alert.alertStyle = .warning

        if cachedOutdated.isEmpty {
            alert.messageText = "Upgrade all outdated packages?"
        } else {
            alert.messageText = "Upgrade \(cachedOutdated.count) package\(cachedOutdated.count == 1 ? "" : "s")?"
        }

        var info = "Apps upgraded as casks (like a web browser) may be closed and replaced while running, without further warning. Save your work first."
        if !cachedOutdated.isEmpty {
            info += "\n\n" + cachedOutdated.joined(separator: "\n")
        }
        alert.informativeText = info

        alert.addButton(withTitle: "Upgrade")
        alert.addButton(withTitle: "Cancel")

        // accessory app: bring the alert to the front, otherwise it can appear behind other windows
        NSApp.activate(ignoringOtherApps: true)
        return alert.runModal() == .alertFirstButtonReturn
    }

    /// allowAutoUpgrade guards against an upgrade loop: packages can stay
    /// outdated after an upgrade (pinned formulae, greedy casks brew declines),
    /// so the fetch that follows an auto-upgrade must not trigger another one.
    func fetchOutdated(allowAutoUpgrade: Bool = true) {
        setMenuBarIcon("brewbar-updating")

        // --greedy also lists casks that self-update (brew skips them by default)
        let command = Settings.includeGreedyCasks ? "outdated --greedy" : "outdated"

        runBrew(command) { output in
            let lines = BrewParser.parseOutdated(output)

            self.cachedOutdated = lines

            if allowAutoUpgrade, !lines.isEmpty,
               Settings.autoUpgradeFormulae || Settings.autoUpgradeCasks
            {
                self.runAutoUpgrade()
                return
            }

            self.updateOutdatedMenu(with: lines)
            self.updateStatus(count: lines.count)
            if lines.isEmpty {
                self.setMenuBarIcon("brewbar-uptodate")
            } else {
                self.setMenuBarIcon("brewbar-outdated")
            }
        }
    }

    func runAutoUpgrade() {
        statusMenuItem.title = "Auto-upgrading..."

        let upgradeCasksThenRefresh = {
            if Settings.autoUpgradeCasks {
                let command = Settings.includeGreedyCasks ? "upgrade --cask --greedy" : "upgrade --cask"
                self.runBrew(command) { _ in
                    self.fetchOutdated(allowAutoUpgrade: false)
                }
            } else {
                self.fetchOutdated(allowAutoUpgrade: false)
            }
        }

        if Settings.autoUpgradeFormulae {
            runBrew("upgrade --formula") { _ in
                upgradeCasksThenRefresh()
            }
        } else {
            upgradeCasksThenRefresh()
        }
    }

    func updateOutdatedMenu(with lines: [String]) {
        outdatedSubmenu.removeAllItems()
        outdatedItem.isHidden = lines.isEmpty
        upgradeAllItem.isHidden = lines.isEmpty

        for formula in lines {
            let item = NSMenuItem(title: formula, action: #selector(upgradeSingle(_:)), keyEquivalent: "")
            item.representedObject = formula
            outdatedSubmenu.addItem(item)
        }
    }

    func updateStatus(count: Int) {
        if count == 0 {
            statusMenuItem.title = "✅ All up to date"
        } else {
            statusMenuItem.title = "⚠️ \(count) outdated"
        }
    }

    @objc func upgradeSingle(_ sender: NSMenuItem) {
        guard let formula = sender.representedObject as? String else { return }

        runBrew("upgrade \(formula)") { _ in
            self.refreshAll()
        }
    }

    func refreshAll() {
        statusMenuItem.title = "Updating..."

        runBrew("update") { _ in
            self.fetchOutdated() // ✅ ONLY place calling outdated
        }
    }

    /// ✅ Brew version (correct)
    func refreshBrewVersion() {
        runBrew("--version") { output in
            let firstLine = BrewParser.parseVersion(output)
            self.versionItem.title = "🏷 \(firstLine)"
        }
    }

    // MARK: - Logs

    /// oldest content is dropped past this size, or the buffer grows forever
    /// and every log() re-renders an ever-larger string into the text view
    let maxLogLength = 100_000

    func log(_ text: String) {
        DispatchQueue.main.async {
            self.logBuffer += text
            if self.logBuffer.count > self.maxLogLength {
                self.logBuffer = String(self.logBuffer.suffix(self.maxLogLength))
            }
            self.logWindow?.update(text: self.logBuffer)
        }
    }

    @objc func showLogs() {
        if logWindow == nil {
            logWindow = LogWindowController()
        }
        logWindow?.showWindow(nil)
        logWindow?.update(text: logBuffer)
    }
}
