//
//  CallApiService.swift
//  humand
//
//  Created by Oleksandr Shumihin on 10/5/25.
//  Copyright © 2025 Humand. All rights reserved.
//

import Foundation
import MMKV
import StreamVideo
import UIKit

@objc
class CallApiService: NSObject {
  private var baseUrl: URL?
  private var keychanKey: String?
  private var tokens: AuthTokens?
  private(set) var streamApiKey: String?
  private(set) var sharedStreamVideo: StreamVideo?
  private(set) var sharedUserId: String?

  @objc static let shared = CallApiService()

  private lazy var mmkv: MMKV? = {
    guard let cryptKey = keychanKey?.data(using: .utf8) else {
      apiLog("Could not convert keychanKey to Data")
      return nil
    }

    guard let mmkv = MMKV(mmapID: "user-storage", cryptKey: cryptKey) else {
      apiLog("Could not initialize MMKV")
      return nil
    }
    return mmkv
  }()

  @objc
  func configure(apiUrl: String, keychanKey: String, streamApiKey: String) {
    guard let url = URL(string: apiUrl) else {
      apiLog("Invalid API URL")
      return
    }

    self.baseUrl = url
    self.keychanKey = keychanKey
    self.streamApiKey = streamApiKey

    MMKV.initialize(rootDir: nil)

    tokens = getTokens()
  }

  @objc
  func setAuthTokens(accessToken: String, refreshToken: String) {
    guard !accessToken.isEmpty, !refreshToken.isEmpty else {
      apiLog("Invalid tokens, clearing existing tokens")
      clearTokens()
      return
    }

    apiLog("Syncing tokens")
    let nextTokens = AuthTokens(accessToken: accessToken, refreshToken: refreshToken)
    updateTokens(tokens: nextTokens)
  }

  @discardableResult
  func reject(id: String) async -> Bool {
    do {
      let (_, response) = try await post(path: "/calls/\(id)/reject")
      if let httpResponse = response as? HTTPURLResponse {
        let success = httpResponse.statusCode == 200
        if !success {
          apiLog("reject failed with status: \(httpResponse.statusCode)")
        }
        return success
      }
      return false
    } catch {
      apiLog("reject error: \(error.localizedDescription)")
      return false
    }
  }

  @discardableResult
  func accept(id: String) async -> Bool {
    do {
      let (_, response) = try await post(path: "/calls/\(id)/accept")
      if let httpResponse = response as? HTTPURLResponse {
        let success = httpResponse.statusCode == 200
        if !success {
          apiLog("accept failed with status: \(httpResponse.statusCode)")
        }
        return success
      }
      return false
    } catch {
      apiLog("accept error: \(error.localizedDescription)")
      return false
    }
  }

  @discardableResult
  func end(id: String) async -> Bool {
    do {
      let (_, response) = try await post(path: "/calls/\(id)/end")
      if let httpResponse = response as? HTTPURLResponse {
        let success = httpResponse.statusCode == 200
        if !success {
          apiLog("end failed with status: \(httpResponse.statusCode)")
        }
        return success
      }
      return false
    } catch {
      apiLog("end error: \(error.localizedDescription)")
      return false
    }
  }

  @discardableResult
  func miss(id: String, participantUserIds: [Int]) async -> Bool {
    do {
      let body: [String: Any] = ["participantUserIds": participantUserIds]
      let (_, response) = try await post(path: "/calls/\(id)/miss", body: body)
      if let httpResponse = response as? HTTPURLResponse {
        let success = httpResponse.statusCode == 200
        if !success {
          apiLog("miss failed with status: \(httpResponse.statusCode)")
        }
        return success
      }
      return false
    } catch {
      apiLog("miss error: \(error.localizedDescription)")
      return false
    }
  }

  func getCallToken() async -> CallTokenResponse? {
    do {
      let (data, response) = try await post(path: "/calls/token")
      if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 200 {
        throw CallConstants.ApiError.requestFailed(statusCode: httpResponse.statusCode)
      }
      let tokenResponse = try JSONDecoder().decode(CallTokenResponse.self, from: data)
      return tokenResponse
    } catch {
      apiLog("getCallToken error: \(error.localizedDescription)")
      return nil
    }
  }

  func getCurrentUser() async -> UserResponse? {
    do {
      let (data, response) = try await get(path: "/janus/me")
      if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 200 {
        throw CallConstants.ApiError.requestFailed(statusCode: httpResponse.statusCode)
      }
      let getMeResponse = try JSONDecoder().decode(GetMeResponse.self, from: data)
      return getMeResponse.user
    } catch {
      apiLog("getCurrentUser error: \(error.localizedDescription)")
      return nil
    }
  }

  @discardableResult
  func registerVoipToken(token: String) async -> Bool {
    do {
      let (_, response) = try await post(path: "/notifications/tokens", body: ["token": token, "provider": "APN"])
      if let httpResponse = response as? HTTPURLResponse {
        let success = (200...299).contains(httpResponse.statusCode)
        if !success {
          apiLog("registerVoipToken failed with status: \(httpResponse.statusCode)")
        }
        return success
      }
      return false
    } catch {
      apiLog("registerVoipToken error: \(error.localizedDescription)")
      return false
    }
  }

  @discardableResult
  func removeVoipToken(token: String) async -> Bool {
    do {
      let (_, response) = try await delete(path: "/notifications/tokens", body: ["token": token, "provider": "APN"])
      if let httpResponse = response as? HTTPURLResponse {
        let success = (200...299).contains(httpResponse.statusCode)
        if !success {
          apiLog("removeVoipToken failed with status: \(httpResponse.statusCode)")
        }
        return success
      }
      return false
    } catch {
      apiLog("removeVoipToken error: \(error.localizedDescription)")
      return false
    }
  }

  /// Get livestream token for host or viewer.
  func getLivestreamToken(isHost: Bool) async -> LivestreamTokenResponse? {
    apiLog("getLivestreamToken: requesting as \(isHost ? "host" : "viewer")")
    do {
      let body: [String: Any]? = isHost ? nil : ["role": "viewer"]
      let (data, response) = try await post(path: "/livestreams/token", body: body)

      if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 200 {
        apiLog("getLivestreamToken failed: HTTP \(httpResponse.statusCode)")
        throw CallConstants.ApiError.requestFailed(statusCode: httpResponse.statusCode)
      }
      let tokenResponse = try JSONDecoder().decode(LivestreamTokenResponse.self, from: data)
      apiLog("getLivestreamToken: got token for participantId=\(tokenResponse.participantId)")
      return tokenResponse
    } catch {
      apiLog("getLivestreamToken exception: \(error.localizedDescription)")
      return nil
    }
  }

  func createStreamClient(
    existingClient: StreamVideo?,
    existingUserId: String?,
    log: @escaping (String) -> Void,
    context: String,
    tokenFetcher: @escaping () async -> (any TokenResponse)?
  ) async throws -> (StreamVideo, String) {
    if let client = existingClient, let existingUserId, !existingUserId.isEmpty {
      return (client, existingUserId)
    }

    // Reuse shared client for calls only (livestreams need different token/role)
    if context != "LivestreamManager", let client = sharedStreamVideo, let userId = sharedUserId, !userId.isEmpty {
      log("createStreamClient[\(context)]: Reusing shared StreamVideo client (userId: \(userId))")
      return (client, userId)
    }

    guard let apiKey = streamApiKey, !apiKey.isEmpty else {
      log("createStreamClient[\(context)]: Missing Stream API key")
      throw NSError(domain: context, code: 1, userInfo: [NSLocalizedDescriptionKey: "Missing Stream API key"])
    }

    guard let tokenResponse = await tokenFetcher() else {
      log("createStreamClient[\(context)]: Failed to get Stream token")
      throw NSError(domain: context, code: 2, userInfo: [NSLocalizedDescriptionKey: "Failed to get Stream token"])
    }

    let userResponse = await getCurrentUser()
    let userName = userResponse?.displayName

    log("Creating Stream user with id: \(tokenResponse.participantId), name: \(userName ?? "nil")")

    LogConfig.level = .debug
    let user = User(id: tokenResponse.participantId, name: userName)
    let client = StreamVideo(
      apiKey: apiKey,
      user: user,
      token: .init(stringLiteral: tokenResponse.token),
      tokenProvider: { result in
        Task {
          if let nextToken = await tokenFetcher() {
            result(.success(.init(stringLiteral: nextToken.token)))
          } else {
            result(.failure(NSError(domain: context, code: 3, userInfo: [NSLocalizedDescriptionKey: "Failed to refresh token"])))
          }
        }
      }
    )

    try await CallApiService.withRetry(logger: log) { try await client.connect() }

    // Only store call clients as shared (livestream clients have different credentials)
    if context != "LivestreamManager" {
      sharedStreamVideo = client
      sharedUserId = tokenResponse.participantId
    }

    return (client, tokenResponse.participantId)
  }

  func disconnectSharedClient() async {
    guard let client = sharedStreamVideo else { return }
    apiLog("disconnectSharedClient: Disconnecting shared StreamVideo client")
    await client.disconnect()
    sharedStreamVideo = nil
    sharedUserId = nil
    apiLog("disconnectSharedClient: done")
  }

  func post(path: String, body: [String: Any]? = nil) async throws -> (Data, URLResponse) {
    try await request(method: "POST", path: path, body: body)
  }

  func get(path: String) async throws -> (Data, URLResponse) {
    try await request(method: "GET", path: path)
  }

  func delete(path: String, body: [String: Any]? = nil) async throws -> (Data, URLResponse) {
    try await request(method: "DELETE", path: path, body: body)
  }

  /// Authenticated request with automatic retry on transient network errors and 401 token refresh.
  private func request(method: String, path: String, body: [String: Any]? = nil) async throws -> (Data, URLResponse) {
    guard let url = baseUrl?.appendingPathComponent(path) else {
      throw CallConstants.ApiError.invalidUrl
    }

    guard let tokens = tokens ?? getTokens() else {
      throw CallConstants.ApiError.missingTokens
    }

    let tryRequest: (String) async throws -> (Data, URLResponse) = { accessToken in
      var request = self.getRequest(url: url, token: accessToken)
      request.httpMethod = method
      if let body = body {
        request.httpBody = try? JSONSerialization.data(withJSONObject: body)
      }
      return try await URLSession.shared.data(for: request)
    }

    var lastError: Error?
    for attempt in 1...CallConstants.Api.maxRetries {
      do {
        let (data, response) = try await tryRequest(tokens.accessToken)

        if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 401 {
          apiLog("Access token expired, trying to refresh...")

          if let json = try? JSONDecoder().decode(DataWithCodeStatus.self, from: data), json.code == CallConstants.Api.tokenExpired {
            guard let nextTokens = await refreshTokenIfNeeded(refreshToken: tokens.refreshToken) else {
              throw CallConstants.ApiError.tokenRefreshFailed
            }
            return try await tryRequest(nextTokens.accessToken)
          }
        }

        return (data, response)
      } catch let error as URLError where error.code == .secureConnectionFailed ||
                                          error.code == .serverCertificateUntrusted ||
                                          error.code == .timedOut ||
                                          error.code == .networkConnectionLost {
        lastError = error
        apiLog("Request failed (attempt \(attempt)/\(CallConstants.Api.maxRetries)): \(error.localizedDescription)")
        if attempt < CallConstants.Api.maxRetries {
          try? await Task.sleep(nanoseconds: CallConstants.Api.retryDelayNs)
        }
      }
    }

    throw lastError ?? CallConstants.ApiError.requestFailed(statusCode: 0)
  }

  private func refreshAccessToken(refreshToken: String) async throws -> AuthTokens {
    guard let url = baseUrl?.appendingPathComponent(CallConstants.Api.refreshTokenURLPath) else {
      throw CallConstants.ApiError.invalidUrl
    }

    let request = getRequest(url: url, token: refreshToken)

    let (data, response) = try await performRequestWithRetry(request)

    guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
      let statusCode = (response as? HTTPURLResponse)?.statusCode ?? 0
      throw CallConstants.ApiError.requestFailed(statusCode: statusCode)
    }

    let tokens = try JSONDecoder().decode(AuthTokens.self, from: data)
    updateTokens(tokens: tokens)

    return tokens
  }

  private func performRequestWithRetry(_ request: URLRequest) async throws -> (Data, URLResponse) {
    var lastError: Error?
    for attempt in 1...CallConstants.Api.maxRetries {
      do {
        return try await URLSession.shared.data(for: request)
      } catch let error as URLError where error.code == .secureConnectionFailed ||
                                          error.code == .serverCertificateUntrusted ||
                                          error.code == .timedOut ||
                                          error.code == .networkConnectionLost {
        lastError = error
        apiLog("Request failed (attempt \(attempt)/\(CallConstants.Api.maxRetries)): \(error.localizedDescription)")
        if attempt < CallConstants.Api.maxRetries {
          try? await Task.sleep(nanoseconds: CallConstants.Api.retryDelayNs)
        }
      }
    }
    throw lastError ?? NSError(domain: "CallApiService", code: -1, userInfo: [NSLocalizedDescriptionKey: "All retry attempts failed"])
  }

  /// Refresh token when needed
  private func refreshTokenIfNeeded(refreshToken: String) async -> AuthTokens? {
    do {
      return try await refreshAccessToken(refreshToken: refreshToken)
    } catch {
      apiLog("Token refresh failed: \(error.localizedDescription)")
      return nil
    }
  }

  private func updateTokens(tokens: AuthTokens) {
    self.tokens = tokens

    do {
      let jsonData = try JSONEncoder().encode(tokens)
      if let jsonString = String(data: jsonData, encoding: .utf8) {
        mmkv?.set(jsonString, forKey: CallConstants.Api.tokensKey)
      } else {
        apiLog("Failed to convert JSON data to String")
      }
    } catch {
      apiLog("Failed to encode tokens: \(error.localizedDescription)")
    }
  }

  private func getTokens()->AuthTokens?{
    guard let jsonString = mmkv?.string(forKey: CallConstants.Api.tokensKey),
          let jsonData = jsonString.data(using: .utf8) else {
      apiLog("Could not find tokens in MMKV")
      return nil
    }

    do {
      let tokens = try JSONDecoder().decode(AuthTokens.self, from: jsonData)
      return tokens
    } catch {
      apiLog("Parsing JSON error: \(error.localizedDescription)")
      return nil
    }
  }

  private func clearTokens() {
    tokens = nil
    apiLog("Tokens removed")
  }

  private static let appVersion = (Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String) ?? "unknown"

  private func getRequest(url:URL, token:String)->URLRequest {
    var request = URLRequest(url: url)
    request.setValue(CallConstants.Api.Headers.contentTypeValue, forHTTPHeaderField: CallConstants.Api.Headers.contentType)
    request.setValue("Bearer \(token)", forHTTPHeaderField: CallConstants.Api.Headers.authorization)
    request.setValue(CallConstants.Api.Headers.originValue, forHTTPHeaderField: CallConstants.Api.Headers.origin)
    request.setValue(CallConstants.Api.Headers.deviceTypeValue, forHTTPHeaderField: CallConstants.Api.Headers.deviceType)
    request.setValue(UIDevice.current.systemVersion, forHTTPHeaderField: CallConstants.Api.Headers.deviceVersion)
    request.setValue(Self.appVersion, forHTTPHeaderField: CallConstants.Api.Headers.humandDeviceVersion)
    return request
  }
}

// MARK: - Shared Helpers

extension CallApiService {
  @discardableResult
  static func withRetry<T>(
    maxAttempts: Int = CallConstants.Retry.defaultMaxAttempts,
    delay: TimeInterval = CallConstants.Retry.defaultDelaySeconds,
    logger: ((String) -> Void)? = nil,
    operation: @escaping () async throws -> T
  ) async throws -> T {
    var lastError: Error?

    for attempt in 1...maxAttempts {
      do {
        return try await operation()
      } catch {
        lastError = error
        logger?("Retry attempt \(attempt)/\(maxAttempts) failed: \(error)")

        if attempt < maxAttempts {
          try? await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
        }
      }
    }

    throw lastError ?? NSError(domain: "Retry", code: -1, userInfo: [NSLocalizedDescriptionKey: "All retry attempts failed"])
  }
}
