import Retry
import UIKit
import Shared
import Combine
import XXModels
import XXClient
import Defaults
import CustomDump
import AppResources
import ReportingFeature
import CombineSchedulers
import XXMessengerClient
import CountryListFeature
import Dependencies
import AppCore

typealias SearchSnapshot = NSDiffableDataSourceSnapshot<SearchSection, SearchItem>

struct SearchLeftViewState {
  var input = ""
  var snapshot: SearchSnapshot?
  var country: Country = .fromMyPhone()
  var item: SearchSegmentedControl.Item = .username
}

final class SearchLeftViewModel {
  @Dependency(\.app.dbManager) var dbManager
  @Dependency(\.app.messenger) var messenger
  @Dependency(\.hudManager) var hudManager
  @Dependency(\.app.toastManager) var toastManager
  @Dependency(\.reportingStatus) var reportingStatus
  @Dependency(\.app.networkMonitor) var networkMonitor

  @KeyObject(.username, defaultValue: nil) var username: String?
  @KeyObject(.sharingEmail, defaultValue: false) var sharingEmail: Bool
  @KeyObject(.sharingPhone, defaultValue: false) var sharingPhone: Bool

  var myId: Data {
    try! messenger.e2e.get()!.getContact().getId()
  }

  var successPublisher: AnyPublisher<XXModels.Contact, Never> {
    successSubject.eraseToAnyPublisher()
  }

  var statePublisher: AnyPublisher<SearchLeftViewState, Never> {
    stateSubject.eraseToAnyPublisher()
  }

  var backgroundScheduler: AnySchedulerOf<DispatchQueue> = DispatchQueue.global().eraseToAnyScheduler()

  var invitation: String?
  private var searchCancellables = Set<AnyCancellable>()
  private let successSubject = PassthroughSubject<XXModels.Contact, Never>()
  private let stateSubject = CurrentValueSubject<SearchLeftViewState, Never>(.init())
  private var networkCancellable = Set<AnyCancellable>()

  init(_ invitation: String? = nil) {
    self.invitation = invitation
  }

  func viewDidAppear() {
    if let pendingInvitation = invitation {
      invitation = nil
      stateSubject.value.input = pendingInvitation
      networkCancellable.removeAll()
      didStartSearching()
    }
  }

  func didEnterInput(_ string: String) {
    stateSubject.value.input = string
  }

  func didPick(country: Country) {
    stateSubject.value.country = country
  }

  func didSelectItem(_ item: SearchSegmentedControl.Item) {
    stateSubject.value.item = item
  }

  func didTapCancelSearch() {
    searchCancellables.forEach { $0.cancel() }
    searchCancellables.removeAll()
    hudManager.hide()
  }

  func didStartSearching() {
    guard stateSubject.value.input.isEmpty == false else { return }

    backgroundScheduler.schedule { [weak self] in
      guard let self else { return }

      self.hudManager.show(.init(
        actionTitle: Localized.Ud.Search.cancel,
        hasDotAnimation: true,
        isAutoDismissable: false,
        onTapClosure: { [weak self] in
          guard let self else { return }
          self.didTapCancelSearch()
        }
      ))

      var content = self.stateSubject.value.input

      if self.stateSubject.value.item == .phone {
        content += self.stateSubject.value.country.code
      }

      enum NodeRegistrationError: Error {
        case unhealthyNet
        case belowMinimum
      }

      retry(max: 5, retryStrategy: .delay(seconds: 2)) { [weak self] in
        guard let self else { return }

        do {
          let nrr = try self.messenger.cMix.get()!.getNodeRegistrationStatus()
          if nrr.ratio < 0.8 { throw NodeRegistrationError.belowMinimum }
        } catch {
          throw NodeRegistrationError.unhealthyNet
        }
      }.finalCatch { [weak self] in
        guard let self else { return }

        if case .unhealthyNet = $0 as? NodeRegistrationError {
          self.hudManager.show(.init(content: "Network is not healthy yet, try again within the next minute or so."))
        } else if case .belowMinimum = $0 as? NodeRegistrationError {
          self.hudManager.show(.init(content:"Node registration ratio is still below 80%, try again within the next minute or so."))
        } else {
          self.hudManager.show(.init(error: $0))
        }

        return
      }

      var factType: FactType = .username

      if self.stateSubject.value.item == .phone {
        factType = .phone
      } else if self.stateSubject.value.item == .email {
        factType = .email
      }

      do {
        let report = try SearchUD.live(
          params: .init(
            e2eId: self.messenger.e2e.get()!.getId(),
            udContact: self.messenger.ud.get()!.getContact(),
            facts: [.init(type: factType, value: content)]
          ),
          callback: .init(handle: {
            switch $0 {
            case .success(let results):
              self.hudManager.hide()
              self.appendToLocalSearch(
                XXModels.Contact(
                  id: try! results.first!.getId(),
                  marshaled: results.first!.data,
                  username: try! results.first?.getFacts().first(where: { $0.type == .username })?.value,
                  email: try? results.first?.getFacts().first(where: { $0.type == .email })?.value,
                  phone: try? results.first?.getFacts().first(where: { $0.type == .phone })?.value,
                  nickname: nil,
                  photo: nil,
                  authStatus: .stranger,
                  isRecent: true,
                  isBlocked: false,
                  isBanned: false,
                  createdAt: Date()
                )
              )
            case .failure(let error):
              print(">>> SearchUD error: \(error.localizedDescription)")

              self.appendToLocalSearch(nil)
              self.hudManager.show(.init(error: error))
            }
          })
        )

        print(">>> UDSearch.Report: \(report))")
      } catch {
        print(">>> UDSearch.Exception: \(error.localizedDescription)")
      }
    }
  }

  func didTapResend(contact: XXModels.Contact) {
    hudManager.show()

    var contact = contact
    contact.authStatus = .requesting

    backgroundScheduler.schedule { [weak self] in
      guard let self else { return }

      do {
        try self.dbManager.getDB().saveContact(contact)

        var includedFacts: [Fact] = []
        let myFacts = try self.messenger.ud.get()!.getFacts()

        if let fact = myFacts.get(.username) {
          includedFacts.append(fact)
        }

        if self.sharingEmail, let fact = myFacts.get(.email) {
          includedFacts.append(fact)
        }

        if self.sharingPhone, let fact = myFacts.get(.phone) {
          includedFacts.append(fact)
        }

        let _ = try self.messenger.e2e.get()!.requestAuthenticatedChannel(
          partner: .live(contact.marshaled!),
          myFacts: includedFacts
        )

        contact.authStatus = .requested
        contact = try self.dbManager.getDB().saveContact(contact)

        self.hudManager.hide()
        self.presentSuccessToast(for: contact, resent: true)
      } catch {
        contact.authStatus = .requestFailed
        _ = try? self.dbManager.getDB().saveContact(contact)
        self.hudManager.show(.init(error: error))
      }
    }
  }

  func didTapRequest(contact: XXModels.Contact) {
    hudManager.show()

    var contact = contact
    contact.nickname = contact.username
    contact.authStatus = .requesting

    backgroundScheduler.schedule { [weak self] in
      guard let self else { return }

      do {
        try self.dbManager.getDB().saveContact(contact)

        var includedFacts: [Fact] = []
        let myFacts = try self.messenger.ud.get()!.getFacts()

        if let fact = myFacts.get(.username) {
          includedFacts.append(fact)
        }

        if self.sharingEmail, let fact = myFacts.get(.email) {
          includedFacts.append(fact)
        }

        if self.sharingPhone, let fact = myFacts.get(.phone) {
          includedFacts.append(fact)
        }

        let _ = try self.messenger.e2e.get()!.requestAuthenticatedChannel(
          partner: .live(contact.marshaled!),
          myFacts: includedFacts
        )

        contact.authStatus = .requested
        contact = try self.dbManager.getDB().saveContact(contact)

        self.hudManager.hide()
        self.successSubject.send(contact)
        self.presentSuccessToast(for: contact, resent: false)
      } catch {
        contact.authStatus = .requestFailed
        _ = try? self.dbManager.getDB().saveContact(contact)
        self.hudManager.show(.init(error: error))
      }
    }
  }

  func didSet(nickname: String, for contact: XXModels.Contact) {
    if var contact = try? dbManager.getDB().fetchContacts(.init(id: [contact.id])).first {
      contact.nickname = nickname
      _ = try? dbManager.getDB().saveContact(contact)
    }
  }

  private func appendToLocalSearch(_ user: XXModels.Contact?) {
    var snapshot = SearchSnapshot()

    if var user = user {
      if let contact = try? dbManager.getDB().fetchContacts(.init(id: [user.id])).first {
        user.isBanned = contact.isBanned
        user.isBlocked = contact.isBlocked
        user.authStatus = contact.authStatus
      }

      if user.authStatus != .friend, !reportingStatus.isEnabled() {
        snapshot.appendSections([.stranger])
        snapshot.appendItems([.stranger(user)], toSection: .stranger)
      } else if user.authStatus != .friend, reportingStatus.isEnabled(), !user.isBanned, !user.isBlocked {
        snapshot.appendSections([.stranger])
        snapshot.appendItems([.stranger(user)], toSection: .stranger)
      }
    }

    let localsQuery = Contact.Query(
      text: stateSubject.value.input,
      authStatus: [.friend],
      isBlocked: reportingStatus.isEnabled() ? false : nil,
      isBanned: reportingStatus.isEnabled() ? false : nil
    )

    if let locals = try? dbManager.getDB().fetchContacts(localsQuery),
       let localsWithoutMe = removeMyself(from: locals),
       localsWithoutMe.isEmpty == false {
      snapshot.appendSections([.connections])
      snapshot.appendItems(
        localsWithoutMe.map(SearchItem.connection),
        toSection: .connections
      )
    }

    stateSubject.value.snapshot = snapshot
  }

  private func removeMyself(from collection: [XXModels.Contact]) -> [XXModels.Contact]? {
    collection.filter { $0.id != myId }
  }

  private func presentSuccessToast(for contact: XXModels.Contact, resent: Bool) {
    let name = contact.nickname ?? contact.username
    let sentTitle = Localized.Requests.Sent.Toast.sent(name ?? "")
    let resentTitle = Localized.Requests.Sent.Toast.resent(name ?? "")

    toastManager.enqueue(.init(
      title: resent ? resentTitle : sentTitle,
      leftImage: Asset.sharedSuccess.image
    ))
  }

  private func waitForNodes(timeout: Int) -> AnyPublisher<Void, Error> {
    Deferred {
      Future { promise in
        retry(max: timeout, retryStrategy: .delay(seconds: 1)) { [weak self] in
          guard let self else { return }
          _ = try self.messenger.cMix.get()!.getNodeRegistrationStatus()
          promise(.success(()))
        }.finalCatch {
          promise(.failure($0))
        }
      }
    }.eraseToAnyPublisher()
  }
}