import Retry
import Models
import XXModels
import Bindings
import Foundation

extension BindingsUserDiscovery: UserDiscoveryInterface {
    public func lookup(forUserId: Data, _ completion: @escaping (Result<Contact, Error>) -> Void) {
        let callback = LookupCallback {
            switch $0 {
            case .success(let contact):
                completion(.success(.init(with: contact, status: .stranger)))
            case .failure(let error):
                completion(.failure(error))
            }
        }

        retry(max: 10, retryStrategy: .delay(seconds: 1)) { [weak self] in
            guard let self = self else { return }
            try self.lookup(forUserId, callback: callback, timeoutMS: 20000)
        }.finalCatch { error in
            log(string: "UD.lookup 4E2E failed:\n\(error.localizedDescription)", type: .error)
            completion(.failure(error.friendly()))
        }
    }

    public func lookup(idList: [Data], _ completion: @escaping (Result<[Contact], Error>) -> Void) {
        let list = BindingsIdList()
        idList.forEach { try? list.add($0) }

        let callback = MultiLookupCallback { [weak self] contactList, idList, error in
            guard let self = self else { return }

            if let error = error, error.count > 2 {
                log(string: "UD.lookup group failed: \(error)", type: .error)
                completion(.failure(NSError.create(error).friendly()))
                return
            }

            guard let contacts = contactList else { return }
            let count = contacts.len()
            var results = [Contact]()

            for index in 0..<count {
                guard let contact = try? contacts.get(index),
                      let marshal = try? contact.marshal(),
                      ((try? self.retrieve(from: marshal, fact: .username) != nil) != nil) else {
                    log(string: "Skipping", type: .error); continue
                }

                results.append(Contact(with: contact, status: .stranger))
            }

            completion(.success(results))
        }

        DispatchQueue.global().async { [weak self] in
            guard let self = self else { return }

            do {
                try self.multiLookup(list, callback: callback, timeoutMS: 30000)
            } catch {
                log(string: "UD.lookup group failed: \(error.localizedDescription)", type: .error)
                completion(.failure(error.friendly()))
            }
        }
    }

    public func deleteMyself(_ username: String) throws {
        log(type: .crumbs)

        do {
            try removeUser("U\(username)")
        } catch {
            throw error.friendly()
        }
    }

    public func register(_ fact: FactType, value: String, _ completion: @escaping (Result<String?, Error>) -> Void) {
        log(type: .crumbs)

        if fact == .username {
            do {
                try register(value)
                completion(.success(value))
                return
            } catch {
                completion(.failure(error.friendly()))
                return
            }
        }

        var error: NSError?
        let bindingsFact = BindingsNewFact(fact.rawValue, value, &error)

        if let error = error {
            completion(.failure(error.friendly()))
            return
        }

        var otherError: NSError?
        let confirmationId = addFact(bindingsFact?.stringify(), error: &otherError)

        if let otherError = otherError {
            completion(.failure(otherError))
            return
        }

        completion(.success(confirmationId))
    }

    public func confirm(code: String, id: String) throws {
        log(type: .crumbs)

        do {
            try confirmFact(id, code: code)
        } catch {
            throw error.friendly()
        }
    }

    public func retrieve(
        from marshaled: Data,
        fact: FactType
    ) throws -> String? {

        log(type: .crumbs)

        var error: NSError?
        let contact = BindingsUnmarshalContact(marshaled, &error)
        if let err = error {
            throw err.friendly()
        }

        return contact?.retrieve(fact: fact)
    }

    public func remove(_ fact: String) throws {
        log(type: .crumbs)

        do {
            try removeFact(fact)
        } catch {
            throw error.friendly()
        }
    }

    public func search(fact: String, _ completion: @escaping (Result<Contact, Error>) -> Void) throws {
        log(type: .crumbs)

        let callback = SearchCallback {
            switch $0 {
            case .success(let contact):
                completion(.success(Contact(with: contact, status: .stranger)))
            case .failure(let error):
                completion(.failure(error))
            }
        }

        do {
            try searchSingle(fact, callback: callback, timeoutMS: 50000)
        } catch {
            throw error.friendly()
        }
    }
}