import Foundation
import LoopKit
import LoopKitUI
import Swinject

protocol PluginManager {
    var availablePumpManagers: [PumpManagerDescriptor] { get }
    var availableCGMManagers: [CGMManagerDescriptor] { get }
    var availableServices: [ServiceDescriptor] { get }
    func getPumpManagerTypeByIdentifier(_ identifier: String) -> PumpManagerUI.Type?
    func getCGMManagerTypeByIdentifier(_ identifier: String) -> CGMManagerUI.Type?
    func getServiceTypeByIdentifier(_ identifier: String) -> ServiceUI.Type?
}

class BasePluginManager: Injectable, PluginManager {
    let pluginBundles: [Bundle]

    init(resolver: Resolver) {
        let pluginsURL: URL? = Bundle.main.privateFrameworksURL
        var bundles = [Bundle]()

        if let pluginsURL = pluginsURL {
            do {
                for pluginURL in try FileManager.default.contentsOfDirectory(at: pluginsURL, includingPropertiesForKeys: nil)
                    .filter({ $0.path.hasSuffix(".framework") })
                {
                    if let bundle = Bundle(url: pluginURL) {
                        if let bname = bundle.object(forInfoDictionaryKey: "CFBundleName") as? String {
                            debug(.deviceManager, "bundle name: \(bname)")
                        }
                        if let bcgm = bundle.object(forInfoDictionaryKey: "com.loopkit.Loop.CGMManagerIdentifier") as? String {
                            debug(.deviceManager, "bundle is CGM: \(bcgm)")
                        }

                        if bundle.isLoopPlugin {
                            debug(.deviceManager, "Found loop plugin:\(pluginURL.absoluteString)")
                            bundles.append(bundle)
                        }
                    }
                }
            } catch {
                debug(.deviceManager, "Error loading plugin: \(error)")
            }
        }
        pluginBundles = bundles
        injectServices(resolver)
    }

    func getPumpManagerTypeByIdentifier(_ identifier: String) -> PumpManagerUI.Type? {
        for bundle in pluginBundles {
            if let name = bundle.object(forInfoDictionaryKey: LoopPluginBundleKey.pumpManagerIdentifier.rawValue) as? String,
               name == identifier
            {
                do {
                    try bundle.loadAndReturnError()

                    if let principalClass = bundle.principalClass as? NSObject.Type {
                        if let plugin = principalClass.init() as? PumpManagerUIPlugin {
                            return plugin.pumpManagerType
                        } else {
                            fatalError("PrincipalClass does not conform to PumpManagerUIPlugin")
                        }

                    } else {
                        fatalError("PrincipalClass not found")
                    }
                } catch {
                    debug(.deviceManager, "Error loading plugin: \(error)")
                }
            }
        }
        return nil
    }

    var availablePumpManagers: [PumpManagerDescriptor] {
        pluginBundles.compactMap({ (bundle) -> PumpManagerDescriptor? in
            guard let title = bundle.object(forInfoDictionaryKey: LoopPluginBundleKey.pumpManagerDisplayName.rawValue) as? String,
                  let identifier = bundle
                  .object(forInfoDictionaryKey: LoopPluginBundleKey.pumpManagerIdentifier.rawValue) as? String
            else {
                return nil
            }

            return PumpManagerDescriptor(identifier: identifier, localizedTitle: title)
        })
    }

    func getCGMManagerTypeByIdentifier(_ identifier: String) -> CGMManagerUI.Type? {
        for bundle in pluginBundles {
            if let name = bundle.object(forInfoDictionaryKey: LoopPluginBundleKey.cgmManagerIdentifier.rawValue) as? String,
               name == identifier
            {
                do {
                    try bundle.loadAndReturnError()

                    if let principalClass = bundle.principalClass as? NSObject.Type {
                        if let plugin = principalClass.init() as? CGMManagerUIPlugin {
                            return plugin.cgmManagerType
                        } else {
                            fatalError("PrincipalClass does not conform to CGMManagerUIPlugin")
                        }

                    } else {
                        fatalError("PrincipalClass not found")
                    }
                } catch {
                    debug(.deviceManager, "Error loading plugin: \(error)")
                }
            }
        }
        return nil
    }

    var availableCGMManagers: [CGMManagerDescriptor] {
        pluginBundles.compactMap({ (bundle) -> CGMManagerDescriptor? in
            guard let title = bundle.object(forInfoDictionaryKey: LoopPluginBundleKey.cgmManagerDisplayName.rawValue) as? String,
                  let identifier = bundle
                  .object(forInfoDictionaryKey: LoopPluginBundleKey.cgmManagerIdentifier.rawValue) as? String
            else {
                return nil
            }

            return CGMManagerDescriptor(identifier: identifier, localizedTitle: title)
        })
    }

    func getServiceTypeByIdentifier(_ identifier: String) -> ServiceUI.Type? {
        for bundle in pluginBundles {
            if let name = bundle.object(forInfoDictionaryKey: LoopPluginBundleKey.serviceIdentifier.rawValue) as? String,
               name == identifier
            {
                do {
                    try bundle.loadAndReturnError()

                    if let principalClass = bundle.principalClass as? NSObject.Type {
                        if let plugin = principalClass.init() as? ServiceUIPlugin {
                            return plugin.serviceType
                        } else {
                            fatalError("PrincipalClass does not conform to ServiceUIPlugin")
                        }

                    } else {
                        fatalError("PrincipalClass not found")
                    }
                } catch {
                    debug(.deviceManager, "Error loading plugin: \(error)")
                }
            }
        }
        return nil
    }

    var availableServices: [ServiceDescriptor] {
        pluginBundles.compactMap({ (bundle) -> ServiceDescriptor? in
            guard let title = bundle.object(forInfoDictionaryKey: LoopPluginBundleKey.serviceDisplayName.rawValue) as? String,
                  let identifier = bundle.object(forInfoDictionaryKey: LoopPluginBundleKey.serviceIdentifier.rawValue) as? String
            else {
                return nil
            }

            return ServiceDescriptor(identifier: identifier, localizedTitle: title)
        })
    }

    func getStatefulPluginTypeByIdentifier(_ identifier: String) -> StatefulPluggable.Type? {
        for bundle in pluginBundles {
            if let name = bundle.object(forInfoDictionaryKey: LoopPluginBundleKey.statefulPluginIdentifier.rawValue) as? String,
               name == identifier
            {
                do {
                    try bundle.loadAndReturnError()

                    if let principalClass = bundle.principalClass as? NSObject.Type {
                        if let plugin = principalClass.init() as? StatefulPlugin {
                            return plugin.pluginType
                        } else {
                            fatalError("PrincipalClass does not conform to StatefulPlugin")
                        }

                    } else {
                        fatalError("PrincipalClass not found")
                    }
                } catch {
                    debug(.deviceManager, "Error loading plugin: \(error)")
                }
            }
        }
        return nil
    }

    var availableStatefulPluginIdentifiers: [String] {
        pluginBundles.compactMap({ (bundle) -> String? in
            bundle.object(forInfoDictionaryKey: LoopPluginBundleKey.statefulPluginIdentifier.rawValue) as? String
        })
    }

    func getOnboardingTypeByIdentifier(_ identifier: String) -> OnboardingUI.Type? {
        for bundle in pluginBundles {
            if let name = bundle.object(forInfoDictionaryKey: LoopPluginBundleKey.onboardingIdentifier.rawValue) as? String,
               name == identifier
            {
                do {
                    try bundle.loadAndReturnError()

                    if let principalClass = bundle.principalClass as? NSObject.Type {
                        if let plugin = principalClass.init() as? OnboardingUIPlugin {
                            return plugin.onboardingType
                        } else {
                            fatalError("PrincipalClass does not conform to OnboardingUIPlugin")
                        }

                    } else {
                        fatalError("PrincipalClass not found")
                    }
                } catch {
                    debug(.deviceManager, "Error loading plugin: \(error)")
                }
            }
        }
        return nil
    }

    var availableOnboardingIdentifiers: [String] {
        pluginBundles.compactMap({ (bundle) -> String? in
            bundle.object(forInfoDictionaryKey: LoopPluginBundleKey.onboardingIdentifier.rawValue) as? String
        })
    }

    func getSupportUITypeByIdentifier(_ identifier: String) -> SupportUI.Type? {
        for bundle in pluginBundles {
            if let name = bundle.object(forInfoDictionaryKey: LoopPluginBundleKey.supportIdentifier.rawValue) as? String,
               name == identifier
            {
                do {
                    try bundle.loadAndReturnError()

                    if let principalClass = bundle.principalClass as? NSObject.Type {
                        if let plugin = principalClass.init() as? SupportUIPlugin {
                            return type(of: plugin.support)
                        } else {
                            fatalError("PrincipalClass does not conform to SupportUIPlugin")
                        }

                    } else {
                        fatalError("PrincipalClass not found")
                    }
                } catch {
                    debug(.deviceManager, "Error loading plugin: \(error)")
                }
            }
        }
        return nil
    }
}

extension Bundle {
    var isPumpManagerPlugin: Bool {
        object(forInfoDictionaryKey: LoopPluginBundleKey.pumpManagerIdentifier.rawValue) as? String != nil }

    var isCGMManagerPlugin: Bool {
        object(forInfoDictionaryKey: LoopPluginBundleKey.cgmManagerIdentifier.rawValue) as? String != nil }

    var isStatefulPlugin: Bool {
        object(forInfoDictionaryKey: LoopPluginBundleKey.statefulPluginIdentifier.rawValue) as? String != nil }

    var isServicePlugin: Bool { object(forInfoDictionaryKey: LoopPluginBundleKey.serviceIdentifier.rawValue) as? String != nil }
    var isOnboardingPlugin: Bool {
        object(forInfoDictionaryKey: LoopPluginBundleKey.onboardingIdentifier.rawValue) as? String != nil }

    var isSupportPlugin: Bool { object(forInfoDictionaryKey: LoopPluginBundleKey.supportIdentifier.rawValue) as? String != nil }

    var isLoopPlugin: Bool {
        isPumpManagerPlugin || isCGMManagerPlugin || isStatefulPlugin || isServicePlugin || isOnboardingPlugin || isSupportPlugin
    }

    var isLoopExtension: Bool { object(forInfoDictionaryKey: LoopPluginBundleKey.extensionIdentifier.rawValue) as? String != nil }

    var isSimulator: Bool { object(forInfoDictionaryKey: LoopPluginBundleKey.pluginIsSimulator.rawValue) as? Bool == true }
}
