import Combine
import ComposableArchitecture
import ElixxirDAppsSDK
import ErrorFeature
import XCTestDynamicOverlay

public struct SessionState: Equatable {
  public init(
    id: UUID,
    networkFollowerStatus: NetworkFollowerStatus? = nil,
    isNetworkHealthy: Bool? = nil,
    error: ErrorState? = nil
  ) {
    self.id = id
    self.networkFollowerStatus = networkFollowerStatus
    self.isNetworkHealthy = isNetworkHealthy
    self.error = error
  }

  public var id: UUID
  public var networkFollowerStatus: NetworkFollowerStatus?
  public var isNetworkHealthy: Bool?
  public var error: ErrorState?
}

public enum SessionAction: Equatable {
  case viewDidLoad
  case updateNetworkFollowerStatus
  case didUpdateNetworkFollowerStatus(NetworkFollowerStatus?)
  case runNetworkFollower(Bool)
  case networkFollowerDidFail(NSError)
  case monitorNetworkHealth(Bool)
  case didUpdateNetworkHealth(Bool?)
  case didDismissError
  case error(ErrorAction)
}

public struct SessionEnvironment {
  public init(
    getCMix: @escaping () -> CMix?,
    bgScheduler: AnySchedulerOf<DispatchQueue>,
    mainScheduler: AnySchedulerOf<DispatchQueue>,
    error: ErrorEnvironment
  ) {
    self.getCMix = getCMix
    self.bgScheduler = bgScheduler
    self.mainScheduler = mainScheduler
    self.error = error
  }

  public var getCMix: () -> CMix?
  public var bgScheduler: AnySchedulerOf<DispatchQueue>
  public var mainScheduler: AnySchedulerOf<DispatchQueue>
  public var error: ErrorEnvironment
}

public let sessionReducer = Reducer<SessionState, SessionAction, SessionEnvironment>
{ state, action, env in
  switch action {
  case .viewDidLoad:
    return .merge([
      .init(value: .updateNetworkFollowerStatus),
      .init(value: .monitorNetworkHealth(true)),
    ])

  case .updateNetworkFollowerStatus:
    return Effect.future { fulfill in
      let status = env.getCMix()?.networkFollowerStatus()
      fulfill(.success(.didUpdateNetworkFollowerStatus(status)))
    }
    .subscribe(on: env.bgScheduler)
    .receive(on: env.mainScheduler)
    .eraseToEffect()

  case .didUpdateNetworkFollowerStatus(let status):
    state.networkFollowerStatus = status
    return .none

  case .runNetworkFollower(let start):
    return Effect.run { subscriber in
      do {
        if start {
          try env.getCMix()?.startNetworkFollower(timeoutMS: 30_000)
        } else {
          try env.getCMix()?.stopNetworkFollower()
        }
      } catch {
        subscriber.send(.networkFollowerDidFail(error as NSError))
      }
      let status = env.getCMix()?.networkFollowerStatus()
      subscriber.send(.didUpdateNetworkFollowerStatus(status))
      subscriber.send(completion: .finished)
      return AnyCancellable {}
    }
    .subscribe(on: env.bgScheduler)
    .receive(on: env.mainScheduler)
    .eraseToEffect()

  case .networkFollowerDidFail(let error):
    state.error = ErrorState(error: error)
    return .none

  case .monitorNetworkHealth(let start):
    struct MonitorEffectId: Hashable {
      var id: UUID
    }
    let effectId = MonitorEffectId(id: state.id)
    if start {
      return Effect.run { subscriber in
        let callback = HealthCallback { isHealthy in
          subscriber.send(.didUpdateNetworkHealth(isHealthy))
        }
        let cancellable = env.getCMix()?.addHealthCallback(callback)
        return AnyCancellable {
          cancellable?.cancel()
        }
      }
      .subscribe(on: env.bgScheduler)
      .receive(on: env.mainScheduler)
      .eraseToEffect()
      .cancellable(id: effectId, cancelInFlight: true)
    } else {
      return Effect.cancel(id: effectId)
        .subscribe(on: env.bgScheduler)
        .eraseToEffect()
    }

  case .didUpdateNetworkHealth(let isHealthy):
    state.isNetworkHealthy = isHealthy
    return .none

  case .didDismissError:
    state.error = nil
    return .none

  case .error(_):
    return .none
  }
}
.presenting(
  errorReducer,
  state: .keyPath(\.error),
  id: .keyPath(\.?.error),
  action: /SessionAction.error,
  environment: \.error
)

extension SessionEnvironment {
  public static let unimplemented = SessionEnvironment(
    getCMix: XCTUnimplemented("\(Self.self).getCMix"),
    bgScheduler: .unimplemented,
    mainScheduler: .unimplemented,
    error: .unimplemented
  )
}