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

    /// 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")
    }

    // 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. Fetch remote SHA256
        guard let remoteSHA = fetchRemoteSHA() else {
            if !silent { showAlert(title: "Update Check Failed", message: "Could not reach the update server.") }
            return
        }

        // 2. Read local SHA256 (written at last install/build)
        let localSHA = readLocalSHA()

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

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

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

        // 6. 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
        }

        // 7. Mount, install, relaunch
        guard installFromDMG() else {
            showAlert(title: "Update Failed", message: "Could not install the update.")
            return
        }

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

        // 9. Relaunch
        relaunch()
    }

    // 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

    private 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: - DMG Install

    private static func installFromDMG() -> Bool {
        let dmg = downloadedDMGPath.path
        let mountPoint = "/tmp/pommedoro-update-mount"

        // 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"
        let dstApp = "/Applications/Pommedoro.app"

        guard FileManager.default.fileExists(atPath: srcApp) else { return false }

        // Remove old and copy new
        try? FileManager.default.removeItem(atPath: dstApp)
        do {
            try FileManager.default.copyItem(atPath: srcApp, toPath: dstApp)
        } catch {
            BugReporting.capture(error)
            return false
        }

        // Strip quarantine
        _ = runShell("xattr -cr \"\(dstApp)\"")

        return true
    }

    // 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: - Relaunch

    private static func relaunch() {
        let appPath = "/Applications/Pommedoro.app"
        let task = Process()
        task.executableURL = URL(fileURLWithPath: "/usr/bin/open")
        task.arguments = ["-n", appPath]
        try? task.run()
        DispatchQueue.main.async {
            NSApp.terminate(nil)
        }
    }

    // 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()
        }
    }
}
