//
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import Foundation

public extension Notification.Name {
    static let isSignalProxyReadyDidChange = Self(SignalProxy.isSignalProxyReadyDidChangeNotificationName)
}

@objc
public class SignalProxy: NSObject {
    @objc
    public static let isSignalProxyReadyDidChangeNotificationName = "isSignalProxyReadyDidChange"

    @objc
    public static var isEnabled: Bool { useProxy && host != nil }

    @objc
    public static var isEnabledAndReady: Bool { isEnabled && relayServer.isReady }

    public static var connectionProxyDictionary: [AnyHashable: Any]? { relayServer.connectionProxyDictionary }

    @Atomic public private(set) static var host: String?

    @Atomic public private(set) static var useProxy = false

    private static let relayServer = RelayServer()

    private static let keyValueStore = SDSKeyValueStore(collection: "SignalProxy")
    private static let proxyHostKey = "proxyHostKey"
    private static let proxyUseKey = "proxyUseKey"

    public static func setProxyHost(host: String?, useProxy: Bool, transaction: SDSAnyWriteTransaction) {
        let hostToStore = host?.nilIfEmpty
        let useProxyToStore = hostToStore == nil ? false : useProxy
        owsAssertDebug(useProxyToStore == useProxy)

        keyValueStore.setString(hostToStore, key: proxyHostKey, transaction: transaction)
        keyValueStore.setBool(useProxyToStore, key: proxyUseKey, transaction: transaction)

        transaction.addSyncCompletion {
            self.host = hostToStore
            self.useProxy = useProxyToStore
            self.ensureProxyState(restartIfNeeded: true)
        }
    }

    @objc
    public class func warmCaches() {
        AppReadiness.runNowOrWhenAppWillBecomeReady {
            databaseStorage.read { transaction in
                host = keyValueStore.getString(proxyHostKey, transaction: transaction)
                useProxy = keyValueStore.getBool(proxyUseKey, defaultValue: false, transaction: transaction)
            }

            NotificationCenter.default.addObserver(self, selector: #selector(applicationDidBecomeActive), name: .OWSApplicationDidBecomeActive, object: nil)

            ensureProxyState()
        }
    }

    @objc
    public class func isValidProxyLink(_ url: URL) -> Bool {
        guard url.user == nil, url.password == nil, url.port == nil else {
            return false
        }

        guard url.host?.caseInsensitiveCompare("signal.tube") == .orderedSame else {
            return false
        }

        guard let scheme = url.scheme else {
            return false
        }
        let isValidScheme = (
            scheme.caseInsensitiveCompare("https") == .orderedSame ||
            scheme.caseInsensitiveCompare("sgnl") == .orderedSame
        )
        guard isValidScheme else {
            return false
        }

        guard isValidProxyFragment(url.fragment) else { return false }

        return true
    }

    public class func isValidProxyFragment(_ fragment: String?) -> Bool {
        guard
            let fragment = fragment?.nilIfEmpty,
            // To quote [RFC 1034][0]: "the total number of octets that represent a domain name
            // [...] is limited to 255." To be extra careful, we set a maximum of 2048.
            // [0]: https://tools.ietf.org/html/rfc1034
            fragment.utf8.count <= 2048,
            let proxyUrl = URL(string: "fake-protocol://\(fragment)"),
            proxyUrl.scheme == "fake-protocol",
            proxyUrl.user == nil || (DebugFlags.internalSettings && proxyUrl.user == "UNENCRYPTED_FOR_TESTING"),
            proxyUrl.password == nil,
            proxyUrl.path.isEmpty,
            proxyUrl.query == nil,
            proxyUrl.fragment == nil,
            proxyUrl.port != 0,
            let proxyHost = proxyUrl.host
        else {
            return false
        }

        // There must be at least 2 domain labels, and none of them can be empty.
        let labels = proxyHost.split(separator: ".", omittingEmptySubsequences: false)
        guard labels.count >= 2 else {
            return false
        }
        guard labels.allSatisfy({ !$0.isEmpty }) else {
            return false
        }

        return true
    }

    @objc
    private class func applicationDidBecomeActive() {
        ensureProxyState()
    }

    private class func ensureProxyState(restartIfNeeded: Bool = false) {
        // The NSE manages the proxy relay itself
        guard !CurrentAppContext().isNSE else { return }

        let libsignalNet = NSObject.networkManager.libsignalNet
        if isEnabled {
            if let (proxyHost, proxyPort) = host.flatMap({ ProxyClient.parseHost($0) }) {
                do {
                    try libsignalNet?.setProxy(host: proxyHost, port: proxyPort)
                } catch {
                    owsFailDebug("failed to set proxy on libsignal-net (need better validation)")
                    // This will poison the Net instance, failing all new connections,
                    // until a valid proxy is set or cleared.
                }
            } else {
                // We can't print the invalid host in the logs, because that's private!
                owsFailDebug("failed to parse previously-validated proxy host")
            }

            if restartIfNeeded && relayServer.isStarted {
                relayServer.restartIfNeeded(ignoreBackoff: true)
            } else {
                relayServer.start()
            }
        } else {
            libsignalNet?.clearProxy()
            relayServer.stop()
        }
    }

    public class func startRelayServer() {
        guard isEnabled else { return }
        Logger.info("Starting the proxy relay server...")
        relayServer.start()
    }

    public class func stopRelayServer() {
        guard isEnabled else { return }
        Logger.info("Stopping the proxy relay server...")
        relayServer.stop()
    }
}
