import Foundation

/// Synor RPC SDK client for Swift.
///
/// Provides blockchain queries, transaction submission, and real-time
/// subscriptions via WebSocket.
///
/// Example:
/// ```swift
/// let rpc = SynorRpc(config: RpcConfig(apiKey: "your-api-key"))
///
/// // Get latest block
/// let block = try await rpc.getLatestBlock()
/// print("Latest block: \(block.height)")
///
/// // Subscribe to new blocks
/// let subscription = try await rpc.subscribeBlocks { block in
///     print("New block: \(block.height)")
/// }
///
/// // Later: cancel subscription
/// subscription.cancel()
/// ```
public class SynorRpc {
    private let config: RpcConfig
    private let session: URLSession
    private let decoder: JSONDecoder
    private let encoder: JSONEncoder

    private var wsTask: URLSessionWebSocketTask?
    private var subscriptions: [String: (String) -> Void] = [:]
    private let subscriptionLock = NSLock()

    public init(config: RpcConfig) {
        self.config = config

        let sessionConfig = URLSessionConfiguration.default
        sessionConfig.timeoutIntervalForRequest = config.timeout
        sessionConfig.timeoutIntervalForResource = config.timeout * 2
        self.session = URLSession(configuration: sessionConfig)

        self.decoder = JSONDecoder()
        self.encoder = JSONEncoder()
    }

    deinit {
        wsTask?.cancel(with: .goingAway, reason: nil)
    }

    // MARK: - Block Operations

    /// Get the latest block.
    public func getLatestBlock() async throws -> Block {
        return try await get(path: "/blocks/latest")
    }

    /// Get a block by hash or height.
    public func getBlock(hashOrHeight: String) async throws -> Block {
        return try await get(path: "/blocks/\(hashOrHeight)")
    }

    /// Get a block header by hash or height.
    public func getBlockHeader(hashOrHeight: String) async throws -> BlockHeader {
        return try await get(path: "/blocks/\(hashOrHeight)/header")
    }

    /// Get blocks in a range.
    public func getBlocks(startHeight: Int64, endHeight: Int64) async throws -> [Block] {
        return try await get(path: "/blocks?start=\(startHeight)&end=\(endHeight)")
    }

    // MARK: - Transaction Operations

    /// Get a transaction by ID.
    public func getTransaction(txid: String) async throws -> RpcTransaction {
        return try await get(path: "/transactions/\(txid)")
    }

    /// Get raw transaction hex.
    public func getRawTransaction(txid: String) async throws -> String {
        let response: RawTxResponse = try await get(path: "/transactions/\(txid)/raw")
        return response.hex
    }

    /// Send a raw transaction.
    public func sendRawTransaction(hex: String) async throws -> SubmitResult {
        let request = SendRawTxRequest(hex: hex)
        return try await post(path: "/transactions/send", body: request)
    }

    /// Decode a raw transaction without broadcasting.
    public func decodeRawTransaction(hex: String) async throws -> RpcTransaction {
        let request = DecodeRawTxRequest(hex: hex)
        return try await post(path: "/transactions/decode", body: request)
    }

    /// Get transactions for an address.
    public func getAddressTransactions(
        address: String,
        limit: Int = 50,
        offset: Int = 0
    ) async throws -> [RpcTransaction] {
        return try await get(path: "/addresses/\(address)/transactions?limit=\(limit)&offset=\(offset)")
    }

    // MARK: - Fee Estimation

    /// Estimate fee for a given priority.
    public func estimateFee(priority: RpcPriority = .medium) async throws -> FeeEstimate {
        return try await get(path: "/fees/estimate?priority=\(priority.rawValue)")
    }

    /// Get all fee estimates.
    public func getAllFeeEstimates() async throws -> [RpcPriority: FeeEstimate] {
        let estimates: [FeeEstimate] = try await get(path: "/fees/estimates")
        return Dictionary(uniqueKeysWithValues: estimates.map { ($0.priority, $0) })
    }

    // MARK: - Chain Information

    /// Get chain information.
    public func getChainInfo() async throws -> ChainInfo {
        return try await get(path: "/chain/info")
    }

    /// Get mempool information.
    public func getMempoolInfo() async throws -> MempoolInfo {
        return try await get(path: "/mempool/info")
    }

    /// Get mempool transactions.
    public func getMempoolTransactions(limit: Int = 100) async throws -> [String] {
        return try await get(path: "/mempool/transactions?limit=\(limit)")
    }

    // MARK: - WebSocket Subscriptions

    /// Subscribe to new blocks.
    public func subscribeBlocks(callback: @escaping (Block) -> Void) async throws -> Subscription {
        return try await subscribe(channel: "blocks", filter: nil) { [weak self] data in
            guard let self = self else { return }
            if let block = try? self.decoder.decode(Block.self, from: Data(data.utf8)) {
                callback(block)
            }
        }
    }

    /// Subscribe to transactions for a specific address.
    public func subscribeAddress(
        address: String,
        callback: @escaping (RpcTransaction) -> Void
    ) async throws -> Subscription {
        return try await subscribe(channel: "address", filter: address) { [weak self] data in
            guard let self = self else { return }
            if let tx = try? self.decoder.decode(RpcTransaction.self, from: Data(data.utf8)) {
                callback(tx)
            }
        }
    }

    /// Subscribe to mempool transactions.
    public func subscribeMempool(callback: @escaping (RpcTransaction) -> Void) async throws -> Subscription {
        return try await subscribe(channel: "mempool", filter: nil) { [weak self] data in
            guard let self = self else { return }
            if let tx = try? self.decoder.decode(RpcTransaction.self, from: Data(data.utf8)) {
                callback(tx)
            }
        }
    }

    private func subscribe(
        channel: String,
        filter: String?,
        callback: @escaping (String) -> Void
    ) async throws -> Subscription {
        try await ensureWebSocketConnection()

        let subscriptionId = UUID().uuidString
        subscriptionLock.lock()
        subscriptions[subscriptionId] = callback
        subscriptionLock.unlock()

        let message = WsMessage(type: "subscribe", channel: channel, data: nil, filter: filter)
        let messageData = try encoder.encode(message)
        let messageString = String(data: messageData, encoding: .utf8)!

        try await wsTask?.send(.string(messageString))

        return Subscription(id: subscriptionId, channel: channel) { [weak self] in
            self?.subscriptionLock.lock()
            self?.subscriptions.removeValue(forKey: subscriptionId)
            self?.subscriptionLock.unlock()

            Task {
                let unsubMessage = WsMessage(type: "unsubscribe", channel: channel, data: nil, filter: nil)
                if let data = try? self?.encoder.encode(unsubMessage),
                   let str = String(data: data, encoding: .utf8) {
                    try? await self?.wsTask?.send(.string(str))
                }
            }
        }
    }

    private func ensureWebSocketConnection() async throws {
        guard wsTask == nil || wsTask?.state != .running else { return }

        guard let url = URL(string: "\(config.wsEndpoint)?token=\(config.apiKey)") else {
            throw RpcError.invalidResponse
        }

        wsTask = session.webSocketTask(with: url)
        wsTask?.resume()

        // Start receiving messages
        receiveMessages()
    }

    private func receiveMessages() {
        wsTask?.receive { [weak self] result in
            guard let self = self else { return }

            switch result {
            case .success(let message):
                switch message {
                case .string(let text):
                    self.handleWebSocketMessage(text)
                case .data(let data):
                    if let text = String(data: data, encoding: .utf8) {
                        self.handleWebSocketMessage(text)
                    }
                @unknown default:
                    break
                }
                // Continue receiving
                self.receiveMessages()

            case .failure(let error):
                if self.config.debug {
                    print("WebSocket error: \(error)")
                }
            }
        }
    }

    private func handleWebSocketMessage(_ text: String) {
        guard let data = text.data(using: .utf8),
              let message = try? decoder.decode(WsMessage.self, from: data),
              message.type == "data",
              let messageData = message.data else {
            return
        }

        subscriptionLock.lock()
        let callbacks = Array(subscriptions.values)
        subscriptionLock.unlock()

        for callback in callbacks {
            callback(messageData)
        }
    }

    // MARK: - HTTP Methods

    private func get<T: Decodable>(path: String) async throws -> T {
        return try await executeWithRetry {
            try await self.performRequest(method: "GET", path: path, body: nil as Empty?)
        }
    }

    private func post<T: Decodable, R: Encodable>(path: String, body: R) async throws -> T {
        return try await executeWithRetry {
            try await self.performRequest(method: "POST", path: path, body: body)
        }
    }

    private func performRequest<T: Decodable, R: Encodable>(
        method: String,
        path: String,
        body: R?
    ) async throws -> T {
        guard let url = URL(string: "\(config.endpoint)\(path)") else {
            throw RpcError.invalidResponse
        }

        var request = URLRequest(url: url)
        request.httpMethod = method
        request.setValue("Bearer \(config.apiKey)", forHTTPHeaderField: "Authorization")
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")

        if let body = body {
            do {
                request.httpBody = try encoder.encode(body)
            } catch {
                throw RpcError.encodingError(error)
            }
        }

        let (data, response): (Data, URLResponse)
        do {
            (data, response) = try await session.data(for: request)
        } catch {
            throw RpcError.networkError(error)
        }

        guard let httpResponse = response as? HTTPURLResponse else {
            throw RpcError.invalidResponse
        }

        if httpResponse.statusCode >= 200 && httpResponse.statusCode < 300 {
            do {
                return try decoder.decode(T.self, from: data)
            } catch {
                throw RpcError.decodingError(error)
            }
        }

        let message = String(data: data, encoding: .utf8) ?? "Unknown error"
        throw RpcError.httpError(statusCode: httpResponse.statusCode, message: message)
    }

    private func executeWithRetry<T>(_ operation: () async throws -> T) async throws -> T {
        var lastError: Error?

        for attempt in 0..<config.retries {
            do {
                return try await operation()
            } catch {
                lastError = error
                if config.debug {
                    print("Attempt \(attempt + 1) failed: \(error)")
                }
                if attempt < config.retries - 1 {
                    try await Task.sleep(nanoseconds: UInt64(1_000_000_000 * (attempt + 1)))
                }
            }
        }

        throw lastError ?? RpcError.invalidResponse
    }
}

// Helper type for empty body
private struct Empty: Encodable {}
