//
//  UpdateManager.swift
//  CmdBar
//
//  Created by Igor Kolokolnikov on 5/23/24.
//

import Foundation
import Zip

struct VersionModel: Codable {
    let ok: Bool
    let version: Double
    let changes: [String]
}

protocol UpdateManagerDelegate: AnyObject {
    func downloadProgressChanged(_ value: Double)
    func unzipProgressChanged(_ value: Double)
}

final class UpdateManager  {
    static let shared = UpdateManager()
    weak var delegate: UpdateManagerDelegate?

    private var observation: NSKeyValueObservation?

    func currentVersion() -> Double? {
        guard let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String else { return nil }
        if let version = Double(version) {
            return version
        } else { return nil }
    }

    func latestVersion() async -> VersionModel? {
        guard let url = URL(string: EndpointConstants.versionURL) else { return nil }
        do {
            let config = URLSessionConfiguration.default
            config.timeoutIntervalForResource = 5.0
            let session = URLSession(configuration: config)
            let (data, _) = try await session.data(from: url)

//            let (data, _) = try await URLSession.shared.data(from: url)
            let response = try JSONDecoder().decode(VersionModel.self, from: data)
            return response.ok ? response : nil
        } catch { return nil }
    }

    func isUpdateAvailable() async -> (available: Bool?, model: VersionModel?) {
        guard let curVersion = currentVersion(),
              let fetVersion = await latestVersion()
        else { return (nil, nil) }

        if fetVersion.version > curVersion {
            return (true, fetVersion)
        }
        return (false, nil)
    }

    func downloadUpdate(completionHandler: @escaping (_ data: Resulting<Bool, String>) -> Void) {
        guard let url = URL(string: EndpointConstants.appURLZip) else { completionHandler(.failure); return }

        let task = URLSession.shared.downloadTask(with: url) { (tempURL, response, error) in
            guard
                let tempURL = tempURL,
                error == nil,
                let response = response as? HTTPURLResponse,
                (200...299).contains(response.statusCode)
            else {
                completionHandler(.failure)
                return
            }

            let fileManager = FileManager.default
            var userTmpURL = FileManager.default.temporaryDirectory

            var isDirectory = ObjCBool(true)
            guard fileManager.fileExists(atPath: userTmpURL.path(), isDirectory: &isDirectory) else { completionHandler(.failure); return }
            userTmpURL = userTmpURL.appendingPathComponent("CmdBar.zip")
            if fileManager.fileExists(atPath: userTmpURL.path()) {
                do {
                    try fileManager.removeItem(at: userTmpURL)
                } catch {
                    completionHandler(.failure)
                    return
                }
            }

            do {
                try fileManager.moveItem(at: tempURL, to: userTmpURL)
                completionHandler(.success)
            } catch {
                completionHandler(.failure)
                return
            }
        }
        observation = task.progress.observe(\.fractionCompleted, changeHandler: { [weak self] progress, _ in
            guard let delegate = self?.delegate else { return }
            delegate.downloadProgressChanged(progress.fractionCompleted)
        })
        task.resume()
    }

    func extractUpdate(completionHandler: @escaping (_ data: Resulting<Bool, String>) -> Void) {
        let fileManager = FileManager.default
        let userTmpURL = fileManager.temporaryDirectory.appending(path: "CmdBar.zip")
        do {
            try Zip.unzipFile(userTmpURL, destination: FileManager.default.temporaryDirectory, overwrite: true, password: nil) { [weak self] progress in
                guard let delegate = self?.delegate else { return }
                delegate.unzipProgressChanged(progress)
            }
            try fileManager.removeItem(at: userTmpURL)
            completionHandler(.success)
        } catch {
            completionHandler(.failure)
            return
        }
    }

    func removeUpdate() {
        let tmp = FileManager.default.temporaryDirectory.appending(path: "CmdBar.app")
        if !existsAtPath(tmp.path()) { return }
        try? FileManager.default.removeItem(at: tmp)
    }

    func installUpdate() {
        if let url = Bundle.main.url(forAuxiliaryExecutable: "cmdbar_updater") {
            let task = Process()
            task.executableURL = url
            try? task.run()
        }
    }

    deinit {
        observation?.invalidate()
    }
}

fileprivate func existsAtPath(_ path: String, isDirectory: Bool = false) -> Bool {
    var isDirectory: ObjCBool = ObjCBool(isDirectory)
    let exists = FileManager.default.fileExists(atPath: path, isDirectory: &isDirectory)
    return exists
}
