import Foundation
import AppKit
import CommonCrypto

/// Handles automatic self-update by comparing the local DMG SHA256 against
/// the one published in the repository. When a mismatch is detected the new
/// DMG is downloaded, mounted, and the app is reinstalled + relaunched.
enum AutoUpdater {

    // MARK: - Configuration

    /// Sentinel value written by `make install` to indicate a dev build.
    /// The updater skips automatic updates when this is the local stamp.
    private static let devSentinel = "dev"

    /// Raw URL for the SHA256 file in the repo (Gitea raw endpoint)
    private static let remoteSHA256URL = URL(
        string: "https://git.nixc.us/colin/pommedoro/raw/branch/main/releases/Pommedoro.dmg.sha256"
    )!

    /// Raw URL for the DMG itself
    private static let remoteDMGURL = URL(
        string: "https://git.nixc.us/colin/pommedoro/raw/branch/main/releases/Pommedoro.dmg"
    )!

    /// Where to store the local copy of the last-known SHA256
    private static var localSHAPath: URL {
        let support = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
        let dir = support.appendingPathComponent("Pommedoro", isDirectory: true)
        try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
        return dir.appendingPathComponent("current.sha256")
    }

    /// Temporary download location for the DMG
    private static var downloadedDMGPath: URL {
        FileManager.default.temporaryDirectory.appendingPathComponent("Pommedoro-update.dmg")
    }

    /// Staging location for the new .app before swap
    private static var stagedAppPath: String {
        NSTemporaryDirectory() + "Pommedoro-staged.app"
    }

    // MARK: - Public API

    /// Check for updates in the background. Optionally show UI feedback.
    static func checkInBackground(silent: Bool = true) {
        DispatchQueue.global(qos: .utility).async {
            self.performCheck(silent: silent)
        }
    }

    // MARK: - Core Logic

    private static func performCheck(silent: Bool) {
        // 1. Read local SHA256 (written at last install/build)
        let localSHA = readLocalSHA()

        // 2. Dev builds are stamped "dev" – skip silent checks entirely
        if localSHA == devSentinel {
            if !silent {
                showAlert(title: "Dev Build", message: "Auto-update is disabled for local dev builds.")
            }
            return
        }

        // 3. Fetch remote SHA256
        guard let remoteSHA = fetchRemoteSHA() else {
            if !silent { showAlert(title: "Update Check Failed", message: "Could not reach the update server.") }
            return
        }

        // 4. Compare
        if remoteSHA == localSHA {
            if !silent { showAlert(title: "Up to Date", message: "You are running the latest version.") }
            return
        }

        // 5. There's an update – prompt user
        let shouldUpdate = promptForUpdate()
        guard shouldUpdate else { return }

        // 6. Download new DMG
        guard downloadDMG() else {
            showAlert(title: "Update Failed", message: "Could not download the update.")
            return
        }

        // 7. Verify downloaded DMG matches remote SHA
        guard let downloadedSHA = sha256(of: downloadedDMGPath), downloadedSHA == remoteSHA else {
            showAlert(title: "Update Failed", message: "Downloaded file integrity check failed.")
            try? FileManager.default.removeItem(at: downloadedDMGPath)
            return
        }

        // 8. Extract new app from DMG to staging area
        guard stageFromDMG() else {
            showAlert(title: "Update Failed", message: "Could not extract the update.")
            return
        }

        // 9. Save new SHA so we don't re-update next launch
        try? remoteSHA.write(to: localSHAPath, atomically: true, encoding: .utf8)

        // 10. Spawn swap script, then terminate so applicationWillTerminate saves state
        spawnSwapAndRelaunch()

        DispatchQueue.main.async {
            NSApp.terminate(nil)
        }
    }

    // MARK: - Networking

    private static func fetchRemoteSHA() -> String? {
        let sem = DispatchSemaphore(value: 0)
        var result: String?

        let task = URLSession.shared.dataTask(with: remoteSHA256URL) { data, response, _ in
            defer { sem.signal() }
            guard let data = data,
                  let http = response as? HTTPURLResponse,
                  http.statusCode == 200,
                  let str = String(data: data, encoding: .utf8) else { return }
            result = str.trimmingCharacters(in: .whitespacesAndNewlines)
        }
        task.resume()
        sem.wait()
        return result
    }

    private static func downloadDMG() -> Bool {
        let sem = DispatchSemaphore(value: 0)
        var success = false

        let task = URLSession.shared.downloadTask(with: remoteDMGURL) { url, response, _ in
            defer { sem.signal() }
            guard let url = url,
                  let http = response as? HTTPURLResponse,
                  http.statusCode == 200 else { return }
            do {
                let dest = downloadedDMGPath
                try? FileManager.default.removeItem(at: dest)
                try FileManager.default.moveItem(at: url, to: dest)
                success = true
            } catch {
                BugReporting.capture(error)
            }
        }
        task.resume()
        sem.wait()
        return success
    }

    // MARK: - SHA256 Computation

    static func sha256(of fileURL: URL) -> String? {
        guard let handle = try? FileHandle(forReadingFrom: fileURL) else { return nil }
        defer { handle.closeFile() }

        var ctx = CC_SHA256_CTX()
        CC_SHA256_Init(&ctx)

        while autoreleasepool(invoking: {
            let chunk = handle.readData(ofLength: 1_048_576) // 1 MB
            if chunk.isEmpty { return false }
            chunk.withUnsafeBytes { ptr in
                _ = CC_SHA256_Update(&ctx, ptr.baseAddress, CC_LONG(chunk.count))
            }
            return true
        }) {}

        var digest = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
        CC_SHA256_Final(&digest, &ctx)
        return digest.map { String(format: "%02x", $0) }.joined()
    }

    // MARK: - Local SHA

    private static func readLocalSHA() -> String? {
        guard let str = try? String(contentsOf: localSHAPath, encoding: .utf8) else { return nil }
        return str.trimmingCharacters(in: .whitespacesAndNewlines)
    }

    /// Write the current SHA256 so subsequent update checks know the baseline.
    static func writeLocalSHA(_ sha: String) {
        try? sha.write(to: localSHAPath, atomically: true, encoding: .utf8)
    }

    // MARK: - Stage from DMG

    /// Mount the downloaded DMG and copy the .app to a temp staging directory.
    /// This avoids replacing the running binary in-place.
    private static func stageFromDMG() -> Bool {
        let dmg = downloadedDMGPath.path
        let mountPoint = "/tmp/pommedoro-update-mount"

        let mountOK = runShell("hdiutil attach \"\(dmg)\" -mountpoint \"\(mountPoint)\" -nobrowse -quiet")
        guard mountOK else { return false }

        defer {
            _ = runShell("hdiutil detach \"\(mountPoint)\" -quiet")
            try? FileManager.default.removeItem(at: downloadedDMGPath)
        }

        let srcApp = "\(mountPoint)/Pommedoro.app"
        guard FileManager.default.fileExists(atPath: srcApp) else { return false }

        // Copy to staging
        try? FileManager.default.removeItem(atPath: stagedAppPath)
        do {
            try FileManager.default.copyItem(atPath: srcApp, toPath: stagedAppPath)
        } catch {
            BugReporting.capture(error)
            return false
        }

        return true
    }

    // MARK: - Swap and Relaunch

    /// Spawns a background shell script that:
    /// 1. Waits for the current process to exit
    /// 2. Replaces /Applications/Pommedoro.app with the staged copy
    /// 3. Strips quarantine
    /// 4. Relaunches the app
    /// The script is fire-and-forget so the current process can terminate cleanly.
    private static func spawnSwapAndRelaunch() {
        let pid = ProcessInfo.processInfo.processIdentifier
        let staged = stagedAppPath
        let dest = "/Applications/Pommedoro.app"

        let script = """
        #!/bin/bash
        # Wait for the old process to exit (up to 10 seconds)
        for i in $(seq 1 20); do
            kill -0 \(pid) 2>/dev/null || break
            sleep 0.5
        done
        # Swap
        rm -rf "\(dest)"
        mv "\(staged)" "\(dest)"
        xattr -cr "\(dest)"
        # Relaunch
        open "\(dest)"
        """

        let scriptPath = NSTemporaryDirectory() + "pommedoro-swap.sh"
        try? script.write(toFile: scriptPath, atomically: true, encoding: .utf8)

        let proc = Process()
        proc.executableURL = URL(fileURLWithPath: "/bin/bash")
        proc.arguments = [scriptPath]
        proc.standardOutput = FileHandle.nullDevice
        proc.standardError = FileHandle.nullDevice
        proc.qualityOfService = .utility
        try? proc.run()
    }

    // MARK: - Shell Helper

    @discardableResult
    private static func runShell(_ command: String) -> Bool {
        let proc = Process()
        proc.executableURL = URL(fileURLWithPath: "/bin/bash")
        proc.arguments = ["-c", command]
        proc.standardOutput = FileHandle.nullDevice
        proc.standardError = FileHandle.nullDevice
        do {
            try proc.run()
            proc.waitUntilExit()
            return proc.terminationStatus == 0
        } catch {
            return false
        }
    }

    // MARK: - User Prompts

    private static func promptForUpdate() -> Bool {
        var result = false
        let sem = DispatchSemaphore(value: 0)
        DispatchQueue.main.async {
            let alert = NSAlert()
            alert.messageText = "Update Available"
            alert.informativeText = "A new version of Pommedoro is available. Would you like to update now?"
            alert.alertStyle = .informational
            alert.addButton(withTitle: "Update")
            alert.addButton(withTitle: "Later")
            result = alert.runModal() == .alertFirstButtonReturn
            sem.signal()
        }
        sem.wait()
        return result
    }

    private static func showAlert(title: String, message: String) {
        DispatchQueue.main.async {
            let alert = NSAlert()
            alert.messageText = title
            alert.informativeText = message
            alert.alertStyle = .informational
            alert.addButton(withTitle: "OK")
            alert.runModal()
        }
    }
}
