// CentrifugoRPCClient.swift
// Centrifugo RPC Client using SwiftCentrifuge
// Auto-generated - DO NOT EDIT

import Foundation
import SwiftCentrifuge

// MARK: - RPC Error Types

/// RPC error type.
public enum RPCError: Error, LocalizedError, Sendable {
    case notConnected
    case timeout
    case serverError(code: Int, message: String)
    case encodingError(Error)
    case decodingError(Error)
    case webSocketError(Error)
    case connectionFailed(String)
    case cancelled

    public var errorDescription: String? {
        switch self {
        case .notConnected:
            return "Not connected to Centrifugo server"
        case .timeout:
            return "RPC request timed out"
        case .serverError(let code, let message):
            return "Server error (\(code)): \(message)"
        case .encodingError(let error):
            return "Failed to encode request: \(error.localizedDescription)"
        case .decodingError(let error):
            return "Failed to decode response: \(error.localizedDescription)"
        case .webSocketError(let error):
            return "WebSocket error: \(error.localizedDescription)"
        case .connectionFailed(let reason):
            return "Connection failed: \(reason)"
        case .cancelled:
            return "Request was cancelled"
        }
    }

    /// Whether this error is retryable.
    /// Transient errors (timeout, network) are retryable.
    /// Permanent errors (auth, encoding) are not.
    public var isRetryable: Bool {
        switch self {
        case .timeout, .webSocketError, .connectionFailed:
            return true
        case .notConnected:
            return false  // Need reconnect first
        case .serverError(let code, _):
            // Retry 5xx server errors, not 4xx client errors
            return code >= 500
        case .encodingError, .decodingError, .cancelled:
            return false
        }
    }

    /// Suggested delay before retry (seconds).
    public var suggestedRetryDelay: TimeInterval {
        switch self {
        case .timeout:
            return 1.0
        case .webSocketError:
            return 2.0
        case .serverError:
            return 3.0
        case .connectionFailed:
            return 2.0
        default:
            return 1.0
        }
    }

    /// User-friendly error message.
    public var userMessage: String {
        switch self {
        case .notConnected:
            return "Not connected. Please check your internet connection."
        case .timeout:
            return "Request timed out. Please try again."
        case .serverError(_, let message):
            return message.isEmpty ? "Server error. Please try again later." : message
        case .webSocketError:
            return "Connection error. Please try again."
        case .connectionFailed:
            return "Unable to connect. Please check your internet connection."
        case .encodingError, .decodingError:
            return "Data error. Please try again or contact support."
        case .cancelled:
            return "Request cancelled."
        }
    }
}

// MARK: - Connection State

/// Centrifugo connection state.
public enum CentrifugoConnectionState: Sendable {
    case disconnected
    case connecting
    case connected
}

// MARK: - Subscription Delegate

/// Internal delegate to handle subscription events with callback closures.
private class SubscriptionDelegate: CentrifugeSubscriptionDelegate {
    let onPublicationHandler: @Sendable (Data) -> Void

    init(onPublication: @escaping @Sendable (Data) -> Void) {
        self.onPublicationHandler = onPublication
    }

    func onPublication(_ sub: CentrifugeSubscription, _ event: CentrifugePublicationEvent) {
        onPublicationHandler(event.data)
    }

    func onSubscribed(_ sub: CentrifugeSubscription, _ event: CentrifugeSubscribedEvent) {
        // Subscription confirmed
    }

    func onSubscribing(_ sub: CentrifugeSubscription, _ event: CentrifugeSubscribingEvent) {
        // Subscribing in progress
    }

    func onUnsubscribed(_ sub: CentrifugeSubscription, _ event: CentrifugeUnsubscribedEvent) {
        // Unsubscribed
    }

    func onError(_ sub: CentrifugeSubscription, _ event: CentrifugeSubscriptionErrorEvent) {
        // Subscription error
    }
}

// MARK: - RPC Client Delegate

/// Internal delegate to capture connection events.
private class RPCClientDelegate: CentrifugeClientDelegate {
    var onConnectedHandler: ((CentrifugeConnectedEvent) -> Void)?
    var onDisconnectedHandler: ((CentrifugeDisconnectedEvent) -> Void)?
    var onConnectingHandler: ((CentrifugeConnectingEvent) -> Void)?
    var onErrorHandler: ((CentrifugeErrorEvent) -> Void)?

    func onConnected(_ client: CentrifugeClient, _ event: CentrifugeConnectedEvent) {
        onConnectedHandler?(event)
    }

    func onDisconnected(_ client: CentrifugeClient, _ event: CentrifugeDisconnectedEvent) {
        onDisconnectedHandler?(event)
    }

    func onConnecting(_ client: CentrifugeClient, _ event: CentrifugeConnectingEvent) {
        onConnectingHandler?(event)
    }

    func onError(_ client: CentrifugeClient, _ event: CentrifugeErrorEvent) {
        onErrorHandler?(event)
    }

    // Server-side subscription events (optional)
    func onSubscribed(_ client: CentrifugeClient, _ event: CentrifugeServerSubscribedEvent) {}
    func onSubscribing(_ client: CentrifugeClient, _ event: CentrifugeServerSubscribingEvent) {}
    func onUnsubscribed(_ client: CentrifugeClient, _ event: CentrifugeServerUnsubscribedEvent) {}
    func onPublication(_ client: CentrifugeClient, _ event: CentrifugeServerPublicationEvent) {}
    func onJoin(_ client: CentrifugeClient, _ event: CentrifugeServerJoinEvent) {}
    func onLeave(_ client: CentrifugeClient, _ event: CentrifugeServerLeaveEvent) {}
}

// MARK: - Centrifugo RPC Client

/// Async RPC client for Centrifugo using SwiftCentrifuge library.
///
/// Usage:
/// ```swift
/// let client = try CentrifugoRPCClient(
///     url: "ws://127.0.0.1:8120/connection/websocket",
///     token: "jwt-token",
///     userId: "user-123"
/// )
/// try await client.connect()
/// let result: MyResult = try await client.call(method: "my.method", params: MyParams(...))
/// await client.disconnect()
/// ```
public actor CentrifugoRPCClient {
    // MARK: - Properties

    private let url: String
    private let token: String
    private let userId: String
    private let timeout: TimeInterval
    private var client: CentrifugeClient?
    private let delegate: RPCClientDelegate

    /// Current connection state.
    public private(set) var state: CentrifugoConnectionState = .disconnected

    // Connection continuations for async/await
    private var connectionContinuation: CheckedContinuation<Void, Error>?

    // MARK: - Initialization

    /// Create a new Centrifugo RPC client.
    ///
    /// - Parameters:
    ///   - url: WebSocket URL of the Centrifugo server
    ///   - token: JWT authentication token
    ///   - userId: User ID for logging/debugging
    ///   - timeout: RPC call timeout in seconds (default: 30)
    public init(
        url: String,
        token: String,
        userId: String,
        timeout: TimeInterval = 30
    ) throws {
        self.url = url
        self.token = token
        self.userId = userId
        self.timeout = timeout
        self.delegate = RPCClientDelegate()
    }

    // MARK: - Connection Management

    /// Connect to the Centrifugo server.
    ///
    /// Establishes WebSocket connection and authenticates with JWT token.
    /// - Throws: RPCError if connection fails
    public func connect() async throws {
        guard state == .disconnected else { return }

        state = .connecting

        // Configure client
        let config = CentrifugeClientConfig(
            token: token,
            useNativeWebSocket: true  // Use URLSessionWebSocketTask
        )

        client = CentrifugeClient(endpoint: url, config: config, delegate: delegate)

        // Set up delegate handlers
        setupDelegateHandlers()

        // Wait for connection
        try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
            connectionContinuation = continuation
            client?.connect()

            // Timeout
            Task {
                try? await Task.sleep(nanoseconds: UInt64(timeout * 1_000_000_000))
                if state == .connecting {
                    connectionContinuation?.resume(throwing: RPCError.timeout)
                    connectionContinuation = nil
                }
            }
        }
    }

    /// Set up delegate handlers for connection events.
    private func setupDelegateHandlers() {
        delegate.onConnectedHandler = { [weak self] event in
            guard let self = self else { return }
            Task {
                await self.handleConnected(event)
            }
        }

        delegate.onDisconnectedHandler = { [weak self] event in
            guard let self = self else { return }
            Task {
                await self.handleDisconnected(event)
            }
        }

        delegate.onErrorHandler = { [weak self] event in
            guard let self = self else { return }
            Task {
                await self.handleError(event)
            }
        }
    }

    private func handleConnected(_ event: CentrifugeConnectedEvent) {
        state = .connected
        connectionContinuation?.resume(returning: ())
        connectionContinuation = nil
    }

    private func handleDisconnected(_ event: CentrifugeDisconnectedEvent) {
        state = .disconnected
        if let continuation = connectionContinuation {
            continuation.resume(throwing: RPCError.connectionFailed("Disconnected: \(event.reason)"))
            connectionContinuation = nil
        }
    }

    private func handleError(_ event: CentrifugeErrorEvent) {
        if let continuation = connectionContinuation {
            continuation.resume(throwing: RPCError.webSocketError(event.error))
            connectionContinuation = nil
        }
    }

    /// Disconnect from the Centrifugo server.
    public func disconnect() async {
        client?.disconnect()
        client = nil
        state = .disconnected
    }

    // MARK: - Subscriptions

    private var subscriptions: [String: CentrifugeSubscription] = [:]
    private var subscriptionDelegates: [String: SubscriptionDelegate] = [:]

    /// Subscribe to a channel for real-time events.
    ///
    /// - Parameters:
    ///   - channel: Channel name to subscribe to
    ///   - onPublication: Callback for incoming messages (raw Data)
    /// - Throws: RPCError if subscription fails
    public func subscribe(
        channel: String,
        onPublication: @escaping @Sendable (Data) -> Void
    ) async throws {
        guard state == .connected, let client = client else {
            throw RPCError.notConnected
        }

        // Create delegate to handle events
        let delegate = SubscriptionDelegate(onPublication: onPublication)
        subscriptionDelegates[channel] = delegate

        // Create subscription with delegate
        let sub = try client.newSubscription(channel: channel, delegate: delegate)

        // Store and subscribe
        subscriptions[channel] = sub
        sub.subscribe()
    }

    /// Unsubscribe from a channel.
    ///
    /// - Parameter channel: Channel name to unsubscribe from
    public func unsubscribe(channel: String) {
        guard let sub = subscriptions[channel] else { return }
        sub.unsubscribe()
        subscriptions.removeValue(forKey: channel)
        subscriptionDelegates.removeValue(forKey: channel)
    }

    /// Unsubscribe from all channels.
    public func unsubscribeAll() {
        for (_, sub) in subscriptions {
            sub.unsubscribe()
        }
        subscriptions.removeAll()
        subscriptionDelegates.removeAll()
    }

    // MARK: - RPC Methods

    /// Call an RPC method and wait for response with timeout.
    ///
    /// - Parameters:
    ///   - method: RPC method name (e.g., "file.list_directory")
    ///   - params: Request parameters (must be Encodable)
    ///   - timeout: Optional timeout in seconds (defaults to client's timeout)
    /// - Returns: Response decoded as the specified type
    /// - Throws: RPCError if the call fails or times out
    public func call<P: Encodable, R: Decodable>(
        method: String,
        params: P,
        timeout requestTimeout: TimeInterval? = nil
    ) async throws -> R {
        guard state == .connected, let client = client else {
            throw RPCError.notConnected
        }

        // Encode params to JSON
        let encoder = JSONEncoder()
        let paramsData: Data
        do {
            paramsData = try encoder.encode(params)
        } catch {
            throw RPCError.encodingError(error)
        }

        // Use provided timeout or fall back to client's default
        let effectiveTimeout = requestTimeout ?? timeout

        // Race between RPC call and timeout
        return try await withThrowingTaskGroup(of: R.self) { group in
            // Task 1: Actual RPC call
            group.addTask {
                try await withCheckedThrowingContinuation { continuation in
                    client.rpc(method: method, data: paramsData) { result in
                        switch result {
                        case .success(let rpcResult):
                            do {
                                let decoder = JSONDecoder()
                                let decoded = try decoder.decode(R.self, from: rpcResult.data)
                                continuation.resume(returning: decoded)
                            } catch {
                                continuation.resume(throwing: RPCError.decodingError(error))
                            }
                        case .failure(let error):
                            continuation.resume(throwing: RPCError.webSocketError(error))
                        }
                    }
                }
            }

            // Task 2: Timeout
            group.addTask {
                try await Task.sleep(nanoseconds: UInt64(effectiveTimeout * 1_000_000_000))
                throw RPCError.timeout
            }

            // Return first to complete, cancel the other
            guard let result = try await group.next() else {
                throw RPCError.timeout
            }
            group.cancelAll()
            return result
        }
    }

    /// Send a fire-and-forget RPC message (no response expected).
    ///
    /// - Parameters:
    ///   - method: RPC method name
    ///   - params: Request parameters
    /// - Throws: RPCError if sending fails
    public func send<P: Encodable>(method: String, params: P) async throws {
        guard state == .connected, let client = client else {
            throw RPCError.notConnected
        }

        // Encode params
        let encoder = JSONEncoder()
        let paramsData: Data
        do {
            paramsData = try encoder.encode(params)
        } catch {
            throw RPCError.encodingError(error)
        }

        // Send without waiting for response
        client.rpc(method: method, data: paramsData) { _ in
            // Ignore result for fire-and-forget
        }
    }
}

// MARK: - Convenience Extensions

extension CentrifugoRPCClient {
    /// Check if connected to the server.
    public var isConnected: Bool {
        state == .connected
    }
}
