import Foundation
import Capacitor
import IonicLiveUpdates

class LiveUpdates {
    private let liveUpdateManager: LiveUpdateManager
    private let existingCacheUrl: URL
    private let staticConfig: LiveUpdateConfiguration
    private(set) var config: LiveUpdateConfiguration

    var enabled: Bool { config.enabled }
    var disabled: Bool { config.disabled }

    var liveUpdate: LiveUpdate {
        config.liveUpdate
    }

    init(_ configJson: JSObject, binaryIsNew: Bool, bundledAppUrl: URL) {
        guard let config = try? JSValueDecoder().decode(LiveUpdateConfiguration.self, from: configJson) else {
            fatalError("Invalid LiveUpdate configuration. Check that both an appId and channel have been provided")
        }

        staticConfig = config
        if binaryIsNew {
            KeyValueStore.standard.removeLiveUpdateConfig()
            self.config = staticConfig
        } else {
            self.config = KeyValueStore.standard.getLiveUpdateConfig() ?? staticConfig
        }

        existingCacheUrl = bundledAppUrl
        let name = "org.getcapacitor.liveupdatesplugin"

        if self.config.enabled, let originalKeyPath = config.key {
            guard let keyFile = originalKeyPath.split(separator: "/").last else {
                fatalError("LiveUpdates - Key name provided was empty")
            }

            guard let keyUrl = Bundle.main.url(forResource: String(keyFile), withExtension: nil) else {
                fatalError("LiveUpdates - Key not found")
            }

            liveUpdateManager = SecureLiveUpdateManager(
                token: self.config.urlToken,
                name: name,
                publicKeyUrl: keyUrl,
                maxVersions: self.config.maxVersions
            )
        } else {
            liveUpdateManager = LiveUpdateManager(
                token: self.config.urlToken,
                name: name,
                maxVersions: self.config.maxVersions
            )
        }

        try? liveUpdateManager.add(self.config.liveUpdate, existingCacheUrl: bundledAppUrl)
    }

    func setServerBasePath(for bridge: any CAPBridgeProtocol) {
        guard let liveUpdatePath = liveUpdateManager.latestAppDirectory(for: config.liveUpdate.appId) else { return }
        bridge.setServerBasePath(liveUpdatePath.path)
    }

    func sync(_ progress: @escaping @Sendable (Double) -> Void) async throws -> LiveUpdateManager.SyncResult {
        try await liveUpdateManager.sync(config.liveUpdate.appId, progress: progress)
    }

    func update(_ options: LiveUpdateConfiguration.Options) throws {
        if config.options == options { return }
        config.update(with: options)
        try liveUpdateManager.add(self.config.liveUpdate, existingCacheUrl: existingCacheUrl)
        KeyValueStore.standard.save(self.config)
    }

    func resetConfig() {
        KeyValueStore.standard.removeLiveUpdateConfig()
        config = staticConfig
        try? liveUpdateManager.add(config.liveUpdate, existingCacheUrl: existingCacheUrl)
    }
}

extension KeyValueStore {
    func removeLiveUpdateConfig() {
        try? delete("liveupdatesconfig")
    }

    func save(_ liveUpdateConfig: LiveUpdateConfiguration) {
        self["liveupdatesconfig"] = liveUpdateConfig
    }

    func getLiveUpdateConfig() -> LiveUpdateConfiguration? {
        self["liveupdatesconfig"]
    }
}

struct LiveUpdateConfiguration: Equatable {
    var liveUpdate: LiveUpdate
    var autoUpdateMethod: AutoUpdateMethod
    var maxVersions: Int
    var enabled: Bool
    var key: String?
    var urlToken: String?

    enum CodingKeys: String, CodingKey {
        case appId, channel, strategy, autoUpdateMethod, maxVersions, enabled, key, urlToken
    }
}

extension LiveUpdateConfiguration: Encodable {
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(liveUpdate.appId, forKey: .appId)
        try container.encode(liveUpdate.channel, forKey: .channel)
        try container.encode(liveUpdate.strategy.rawValue, forKey: .strategy)
        try container.encode(autoUpdateMethod.rawValue, forKey: .autoUpdateMethod)
        try container.encode(maxVersions, forKey: .maxVersions)
        try container.encode(enabled, forKey: .enabled)
    }
}

extension LiveUpdateConfiguration: Decodable {
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let appId = try container.decode(String.self, forKey: .appId)
        let channel = try container.decode(String.self, forKey: .channel)
        let strategy = try container.decodeIfPresent(String.self, forKey: .strategy)
            .flatMap(LiveUpdate.Strategy.init)
            ?? .differential

        autoUpdateMethod = try container.decodeIfPresent(String.self, forKey: .autoUpdateMethod)
            .flatMap(AutoUpdateMethod.init)
            ?? .background

        liveUpdate = LiveUpdate(
            appId: appId,
            channel: channel,
            syncOnAdd: autoUpdateMethod == .background,
            strategy: strategy
        )

        maxVersions = try container.decodeIfPresent(Int.self, forKey: .maxVersions) ?? 3
        key = try container.decodeIfPresent(String.self, forKey: .key)
        enabled = try container.decodeIfPresent(Bool.self, forKey: .enabled) ?? true
        urlToken = try container.decodeIfPresent(String.self, forKey: .urlToken)
    }
}

extension LiveUpdateConfiguration {
    var options: Options {
        Options(
            appId: liveUpdate.appId,
            channel: liveUpdate.channel,
            strategy: liveUpdate.strategy,
            autoUpdateMethod: autoUpdateMethod,
            maxVersions: maxVersions,
            enabled: enabled
        )
    }

    struct Options: Codable, Equatable {
        var appId: String?
        var channel: String?
        var strategy: LiveUpdate.Strategy?
        var autoUpdateMethod: AutoUpdateMethod?
        var maxVersions: Int?
        var enabled: Bool?

        var syncOnAdd: Bool? {
            guard let autoUpdateMethod else { return nil }
            return autoUpdateMethod == .background
        }

    }

    mutating func update(with options: Options) {
        liveUpdate.appId ?= options.appId
        liveUpdate.channel ?= options.channel
        liveUpdate.strategy ?= options.strategy
        liveUpdate.syncOnAdd ?= options.syncOnAdd
        autoUpdateMethod ?= options.autoUpdateMethod
    }
}

infix operator ?=
private func ?=<A>(_ lhs: inout A, _ rhs: A?) where A: Equatable {
    if let rhs {
        lhs = rhs
    }
}

extension LiveUpdateConfiguration {
    var disabled: Bool { !enabled }
}

enum AutoUpdateMethod: String, Codable {
    case none, background

    init?(rawValue: String) {
        switch rawValue.lowercased() {
        case "none":
            self = .none
        case "background":
            self = .background
        default:
            return nil
        }
    }
}

extension IonicLiveUpdates.LiveUpdateManager.SyncResult: Swift.Encodable {
    enum TopLevelKeys: String, CodingKey { case liveUpdate, snapshot, source, activeApplicationPathChanged }
    enum LiveUpdateKeys: String, CodingKey { case appId, channel }
    enum SnapshotKeys: String, CodingKey { case id, buildId }

    public func encode(to encoder: Encoder) throws {
        var topLevelContainer = encoder.container(keyedBy: TopLevelKeys.self)

        var liveUpdateContainer = topLevelContainer.nestedContainer(keyedBy: LiveUpdateKeys.self, forKey: .liveUpdate)
        try liveUpdateContainer.encode(liveUpdate.appId, forKey: .appId)
        try liveUpdateContainer.encode(liveUpdate.channel, forKey: .channel)

        if let snapshot {
            var snapshotContainer = topLevelContainer.nestedContainer(keyedBy: SnapshotKeys.self, forKey: .snapshot)
            try snapshotContainer.encode(snapshot.id, forKey: .id)
            try snapshotContainer.encode(snapshot.buildId, forKey: .buildId)
        }

        if case let .cache(pathsChanged) = source {
            try topLevelContainer.encode("cache", forKey: .source)
            try topLevelContainer.encode(pathsChanged, forKey: .activeApplicationPathChanged)
        } else {
            try topLevelContainer.encode("download", forKey: .source)
            try topLevelContainer.encode(true, forKey: .activeApplicationPathChanged)
        }
    }
}

extension IonicLiveUpdates.LiveUpdateManager.SyncError: Swift.Encodable {
    enum CodingKeys: String, CodingKey { case appId, failStep, message }

    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        let failStep: String
        if case .secureUpdateError = reason {
            failStep = "VERIFY"
        } else {
            failStep = self.failStep.rawValue.uppercased()
        }

        try container.encode(appId, forKey: .appId)
        try container.encode(failStep, forKey: .failStep)
        try container.encode(errorDescription, forKey: .message)
    }
}

extension SecureLiveUpdateManager {
    convenience init(token: String?, name: String, publicKeyUrl: URL, maxVersions: Int) {
        if let token {
            do {
                try self.init(urlToken: token, publicKeyUrl: publicKeyUrl, name: name, maxVersions: maxVersions)
            } catch {
                fatalError("Failed to initialize SecureLiveUpdateManager with urlToken: \(error.localizedDescription)")
            }
        } else {
            self.init(named: name, publicKeyUrl: publicKeyUrl, maxVersions: maxVersions)
        }
    }
}

extension LiveUpdateManager {
    convenience init(token: String?, name: String, maxVersions: Int) {
        if let token {
            do {
                try self.init(urlToken: token, name: name, maxVersions: maxVersions)
            } catch {
                fatalError("Failed to initialize LiveUpdateManager with urlToken: \(error.localizedDescription)")
            }
        } else {
            self.init(named: name, maxVersions: maxVersions)
        }
    }
}
