import BackendEnvironmentClient
import ComposableArchitecture
import Foundation
import HCaptcha
import UIKit
import Utilities

@DependencyClient
public struct HCaptchaClient {
    /// Initiates a background task to prefetch an HCaptcha token.
    /// This is a fire-and-forget operation that silently fails if token generation fails.
    /// Use this to improve perceived performance by having a token ready before it's needed.
    public var prepareToken: () -> Void

    /// Retrieves a valid hCaptcha token, generating a new one if necessary.
    /// This function will either return a cached token if valid, or generate a new one.
    /// Throws HCaptchaError if token generation fails.
    public var getToken: () async throws -> String

    private static func timestamp() -> String {
        let date = Date()
        let calendar = Calendar.current
        let minutes = calendar.component(.minute, from: date)
        let seconds = calendar.component(.second, from: date)
        let milliseconds = calendar.component(.nanosecond, from: date) / 1_000_000
        return String(format: "%02d:%02d:%03d", minutes, seconds, milliseconds)
    }

    private static func debugLog(_ message: String) {
        #if DEBUG
            print("[\(HCaptchaClient.timestamp())] [HCaptcha Debug] \(message)")
        #endif
    }

    private static func fetchHCaptchaToken(apiKey: String, domain: String) async throws -> String {
        debugLog("Attempting to fetch HCaptcha token")
        let hcaptcha: HCaptcha
        do {
            guard let baseURL = URL(string: "https://\(domain)") else {
                debugLog("Invalid HCaptcha domain: \(domain)")
                throw HCaptchaError.initializationError("Invalid HCaptcha domain: \(domain)")
            }
            hcaptcha = try HCaptcha(
                apiKey: apiKey,
                passiveApiKey: true,
                baseURL: baseURL
            )
        } catch {
            debugLog("Failed to initialize HCaptcha: \(error.localizedDescription)")
            throw HCaptchaError.initializationError(error.localizedDescription)
        }
        return try await withCheckedThrowingContinuation { continuation in
            var isValidating = false
            hcaptcha.validate { result in
                guard !isValidating else { return }
                isValidating = true

                do {
                    let newToken = try result.dematerialize()
                    debugLog("Successfully fetched HCaptcha token")
                    continuation.resume(returning: newToken)
                } catch {
                    debugLog("Error fetching HCaptcha token: \(error.localizedDescription)")
                    continuation.resume(throwing: HCaptchaError.tokenGenerationError(error.localizedDescription))
                }
            }
        }
    }
}

extension HCaptchaClient: DependencyKey {
    public static let liveValue: Self = {
        @Dependency(BackendEnvironmentClient.self) var backendEnvironment

        // Configuration is captured at initialization and remains fixed for the app's lifetime.
        // Environment changes require app restart (via DebugMenu), which reinitializes this dependency with the new config.
        let config = backendEnvironment.configuration()

        var currentTokenGenerationRequest: Task<String, Error>?
        var token: String?
        var tokenExpiration: Date?
        let tokenExpirationInterval: TimeInterval = 12 * 60 // 12 minutes

        func generateNewToken(forceRequest: Bool) async throws -> String {
            debugLog("Generating new token with forceRequest: \(forceRequest)")
            if let existingTokenGenerationRequest = currentTokenGenerationRequest {
                debugLog("Returning existing token generation request")
                return try await existingTokenGenerationRequest.value
            }
            let newTokenGenerationRequest = forceRequest ? Task {
                debugLog("Forcing new token generation")
                let hCaptchaToken = try await fetchHCaptchaToken(apiKey: config.hcaptchaKey, domain: config.hcaptchaDomain)
                let newTokenExpiration = Date(timeIntervalSinceNow: tokenExpirationInterval)
                setToken(hCaptchaToken, expiration: newTokenExpiration)
                return hCaptchaToken
            } : Task.detached {
                debugLog("Prefetching new token")
                let hCaptchaToken = try await fetchHCaptchaToken(apiKey: config.hcaptchaKey, domain: config.hcaptchaDomain)
                let newTokenExpiration = Date(timeIntervalSinceNow: tokenExpirationInterval)
                setToken(hCaptchaToken, expiration: newTokenExpiration)
                return hCaptchaToken
            }

            currentTokenGenerationRequest = newTokenGenerationRequest

            do {
                let result = try await newTokenGenerationRequest.value
                debugLog("Token generation successful")
                invalidateTokenGenerationRequest()
                return result
            } catch {
                debugLog("Token generation failed: \(error.localizedDescription)")
                invalidateTokenGenerationRequest()
                invalidateToken()
                throw HCaptchaError.tokenGenerationError(error.localizedDescription)
            }
        }

        func setToken(_ newToken: String, expiration: Date) {
            token = newToken
            tokenExpiration = expiration
        }

        func invalidateToken() {
            token = nil
            tokenExpiration = nil
        }

        func invalidateTokenGenerationRequest() {
            currentTokenGenerationRequest = nil
        }

        return Self(
            prepareToken: {
                debugLog("Preparing token")
                _ = Task.detached {
                    do {
                        _ = try await generateNewToken(forceRequest: false)
                        debugLog("Token prepared successfully")
                    } catch {
                        debugLog("Token preparation failed: \(error.localizedDescription)")
                        // Silently fail when prefetching
                    }
                }
            },
            getToken: {
                debugLog("Retrieving token")
                if let existingToken = token,
                   let expiration = tokenExpiration,
                   Date() < expiration
                {
                    debugLog("Returning cached token")
                    invalidateToken()
                    return existingToken
                }

                do {
                    let newToken = try await generateNewToken(forceRequest: true)
                    debugLog("New token retrieved successfully")
                    invalidateToken()
                    return newToken
                } catch {
                    debugLog("Failed to retrieve token: \(error.localizedDescription)")
                    throw HCaptchaError.tokenGenerationError(error.localizedDescription)
                }
            }
        )
    }()
}

extension HCaptchaClient: TestDependencyKey {
    public static let previewValue = Self(
        prepareToken: {},
        getToken: { "test-token" }
    )

    public static let testValue = Self(
        prepareToken: {},
        getToken: { "test-token" }
    )
}
