import Foundation

/// Synor IBC SDK for Swift
///
/// Inter-Blockchain Communication (IBC) protocol for cross-chain interoperability.
public class SynorIbc {
    private let config: IbcConfig
    private let session: URLSession
    private var _closed = false

    public lazy var clients = LightClientClient(ibc: self)
    public lazy var connections = ConnectionsClient(ibc: self)
    public lazy var channels = ChannelsClient(ibc: self)
    public lazy var transfer = TransferClient(ibc: self)
    public lazy var swaps = SwapsClient(ibc: self)

    public init(config: IbcConfig) {
        self.config = config
        let sessionConfig = URLSessionConfiguration.default
        sessionConfig.timeoutIntervalForRequest = TimeInterval(config.timeout)
        self.session = URLSession(configuration: sessionConfig)
    }

    public var chainId: String { config.chainId }

    public func getChainInfo() async throws -> [String: Any] {
        try await get("/chain")
    }

    public func getHeight() async throws -> Height {
        let result = try await get("/chain/height")
        return Height(
            revisionNumber: (result["revision_number"] as? UInt64) ?? 0,
            revisionHeight: (result["revision_height"] as? UInt64) ?? 1
        )
    }

    public func healthCheck() async -> Bool {
        do {
            let result = try await get("/health")
            return result["status"] as? String == "healthy"
        } catch {
            return false
        }
    }

    public func close() {
        _closed = true
        session.invalidateAndCancel()
    }

    public var isClosed: Bool { _closed }

    // Internal HTTP methods
    func get(_ path: String) async throws -> [String: Any] {
        try checkClosed()
        var request = URLRequest(url: URL(string: "\(config.endpoint)\(path)")!)
        request.httpMethod = "GET"
        addHeaders(&request)
        return try await performRequest(request)
    }

    func post(_ path: String, body: [String: Any]) async throws -> [String: Any] {
        try checkClosed()
        var request = URLRequest(url: URL(string: "\(config.endpoint)\(path)")!)
        request.httpMethod = "POST"
        request.httpBody = try JSONSerialization.data(withJSONObject: body)
        addHeaders(&request)
        return try await performRequest(request)
    }

    private func addHeaders(_ request: inout URLRequest) {
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.setValue("Bearer \(config.apiKey)", forHTTPHeaderField: "Authorization")
        request.setValue("swift/0.1.0", forHTTPHeaderField: "X-SDK-Version")
        request.setValue(config.chainId, forHTTPHeaderField: "X-Chain-Id")
    }

    private func performRequest(_ request: URLRequest) async throws -> [String: Any] {
        let (data, response) = try await session.data(for: request)
        guard let httpResponse = response as? HTTPURLResponse else {
            throw IbcError("Invalid response")
        }
        if httpResponse.statusCode >= 400 {
            let error = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
            throw IbcError(
                error?["message"] as? String ?? "HTTP \(httpResponse.statusCode)",
                code: error?["code"] as? String,
                status: httpResponse.statusCode
            )
        }
        guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
            throw IbcError("Invalid JSON response")
        }
        return json
    }

    private func checkClosed() throws {
        if _closed {
            throw IbcError("Client has been closed", code: "CLIENT_CLOSED")
        }
    }

    /// Light client sub-client
    public class LightClientClient {
        private let ibc: SynorIbc

        init(ibc: SynorIbc) {
            self.ibc = ibc
        }

        public func create(
            clientType: ClientType,
            clientState: ClientState,
            consensusState: [String: Any]
        ) async throws -> ClientId {
            let result = try await ibc.post("/clients", body: [
                "client_type": clientType.rawValue,
                "client_state": [
                    "chain_id": clientState.chainId,
                    "trust_level": [
                        "numerator": clientState.trustLevel.numerator,
                        "denominator": clientState.trustLevel.denominator
                    ],
                    "trusting_period": clientState.trustingPeriod,
                    "unbonding_period": clientState.unbondingPeriod,
                    "max_clock_drift": clientState.maxClockDrift,
                    "latest_height": [
                        "revision_number": clientState.latestHeight.revisionNumber,
                        "revision_height": clientState.latestHeight.revisionHeight
                    ]
                ] as [String: Any],
                "consensus_state": consensusState
            ])
            guard let clientId = result["client_id"] as? String else {
                throw IbcError("Missing client_id in response")
            }
            return ClientId(clientId)
        }

        public func getState(clientId: ClientId) async throws -> ClientState {
            let result = try await ibc.get("/clients/\(clientId.id)/state")
            guard let chainId = result["chain_id"] as? String,
                  let trustLevelDict = result["trust_level"] as? [String: Any],
                  let latestHeightDict = result["latest_height"] as? [String: Any] else {
                throw IbcError("Invalid client state response")
            }
            return ClientState(
                chainId: chainId,
                trustLevel: TrustLevel(
                    numerator: trustLevelDict["numerator"] as? Int ?? 1,
                    denominator: trustLevelDict["denominator"] as? Int ?? 3
                ),
                trustingPeriod: result["trusting_period"] as? UInt64 ?? 0,
                unbondingPeriod: result["unbonding_period"] as? UInt64 ?? 0,
                maxClockDrift: result["max_clock_drift"] as? UInt64 ?? 0,
                latestHeight: Height(
                    revisionNumber: latestHeightDict["revision_number"] as? UInt64 ?? 0,
                    revisionHeight: latestHeightDict["revision_height"] as? UInt64 ?? 1
                )
            )
        }

        public func list() async throws -> [[String: Any]] {
            let result = try await ibc.get("/clients")
            return result["clients"] as? [[String: Any]] ?? []
        }
    }

    /// Connections sub-client
    public class ConnectionsClient {
        private let ibc: SynorIbc

        init(ibc: SynorIbc) {
            self.ibc = ibc
        }

        public func openInit(
            clientId: ClientId,
            counterpartyClientId: ClientId
        ) async throws -> ConnectionId {
            let result = try await ibc.post("/connections/init", body: [
                "client_id": clientId.id,
                "counterparty_client_id": counterpartyClientId.id
            ])
            guard let connectionId = result["connection_id"] as? String else {
                throw IbcError("Missing connection_id in response")
            }
            return ConnectionId(connectionId)
        }

        public func get(connectionId: ConnectionId) async throws -> [String: Any] {
            try await ibc.get("/connections/\(connectionId.id)")
        }

        public func list() async throws -> [[String: Any]] {
            let result = try await ibc.get("/connections")
            return result["connections"] as? [[String: Any]] ?? []
        }
    }

    /// Channels sub-client
    public class ChannelsClient {
        private let ibc: SynorIbc

        init(ibc: SynorIbc) {
            self.ibc = ibc
        }

        public func bindPort(portId: PortId, module: String) async throws {
            _ = try await ibc.post("/ports/bind", body: [
                "port_id": portId.id,
                "module": module
            ])
        }

        public func openInit(
            portId: PortId,
            ordering: ChannelOrder,
            connectionId: ConnectionId,
            counterpartyPort: PortId,
            version: String
        ) async throws -> ChannelId {
            let result = try await ibc.post("/channels/init", body: [
                "port_id": portId.id,
                "ordering": ordering.rawValue,
                "connection_id": connectionId.id,
                "counterparty_port": counterpartyPort.id,
                "version": version
            ])
            guard let channelId = result["channel_id"] as? String else {
                throw IbcError("Missing channel_id in response")
            }
            return ChannelId(channelId)
        }

        public func get(portId: PortId, channelId: ChannelId) async throws -> [String: Any] {
            try await ibc.get("/channels/\(portId.id)/\(channelId.id)")
        }

        public func list() async throws -> [[String: Any]] {
            let result = try await ibc.get("/channels")
            return result["channels"] as? [[String: Any]] ?? []
        }
    }

    /// Transfer sub-client (ICS-20)
    public class TransferClient {
        private let ibc: SynorIbc

        init(ibc: SynorIbc) {
            self.ibc = ibc
        }

        public func transfer(
            sourcePort: String,
            sourceChannel: String,
            denom: String,
            amount: String,
            sender: String,
            receiver: String,
            timeout: Timeout? = nil,
            memo: String? = nil
        ) async throws -> [String: Any] {
            var body: [String: Any] = [
                "source_port": sourcePort,
                "source_channel": sourceChannel,
                "token": ["denom": denom, "amount": amount],
                "sender": sender,
                "receiver": receiver
            ]
            if let timeout = timeout {
                body["timeout_height"] = [
                    "revision_number": timeout.height.revisionNumber,
                    "revision_height": timeout.height.revisionHeight
                ]
                body["timeout_timestamp"] = String(timeout.timestamp)
            }
            if let memo = memo { body["memo"] = memo }
            return try await ibc.post("/transfer", body: body)
        }

        public func getDenomTrace(ibcDenom: String) async throws -> [String: Any] {
            try await ibc.get("/transfer/denom_trace/\(ibcDenom)")
        }
    }

    /// Swaps sub-client (HTLC)
    public class SwapsClient {
        private let ibc: SynorIbc

        init(ibc: SynorIbc) {
            self.ibc = ibc
        }

        public func initiate(
            responder: String,
            initiatorAsset: [String: Any],
            responderAsset: [String: Any]
        ) async throws -> [String: Any] {
            try await ibc.post("/swaps/initiate", body: [
                "responder": responder,
                "initiator_asset": initiatorAsset,
                "responder_asset": responderAsset
            ])
        }

        public func lock(swapId: SwapId) async throws {
            _ = try await ibc.post("/swaps/\(swapId.id)/lock", body: [:])
        }

        public func respond(swapId: SwapId, asset: [String: Any]) async throws -> [String: Any] {
            try await ibc.post("/swaps/\(swapId.id)/respond", body: ["asset": asset])
        }

        public func claim(swapId: SwapId, secret: Data) async throws -> [String: Any] {
            try await ibc.post("/swaps/\(swapId.id)/claim", body: [
                "secret": secret.base64EncodedString()
            ])
        }

        public func refund(swapId: SwapId) async throws -> [String: Any] {
            try await ibc.post("/swaps/\(swapId.id)/refund", body: [:])
        }

        public func get(swapId: SwapId) async throws -> AtomicSwap {
            let result = try await ibc.get("/swaps/\(swapId.id)")
            guard let swap = AtomicSwap.fromJson(result) else {
                throw IbcError("Invalid swap response")
            }
            return swap
        }

        public func listActive() async throws -> [AtomicSwap] {
            let result = try await ibc.get("/swaps/active")
            guard let swaps = result["swaps"] as? [[String: Any]] else {
                return []
            }
            return swaps.compactMap { AtomicSwap.fromJson($0) }
        }
    }
}
