import Foundation

/// Synor Bridge SDK client for Swift.
///
/// Provides cross-chain asset transfers with lock-mint and burn-unlock patterns.
///
/// Example:
/// ```swift
/// let bridge = SynorBridge(config: BridgeConfig(apiKey: "your-api-key"))
///
/// // Get supported chains
/// let chains = try await bridge.getSupportedChains()
///
/// // Estimate fee
/// let fee = try await bridge.estimateFee(
///     asset: "SYNR",
///     amount: "1000000000000000000",
///     sourceChain: .synor,
///     targetChain: .ethereum
/// )
///
/// // Full bridge flow
/// let transfer = try await bridge.bridgeTo(
///     asset: "SYNR",
///     amount: "1000000000000000000",
///     targetChain: .ethereum,
///     targetAddress: "0x..."
/// )
/// print("Transfer completed: \(transfer.id)")
/// ```
public class SynorBridge {
    private let config: BridgeConfig
    private let session: URLSession
    private let decoder: JSONDecoder
    private let encoder: JSONEncoder
    private var closed = false

    private static let finalStatuses: Set<TransferStatus> = [.completed, .failed, .refunded]

    public init(config: BridgeConfig) {
        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()
    }

    // MARK: - Chain Operations

    /// Get all supported chains.
    public func getSupportedChains() async throws -> [Chain] {
        let response: ChainsResponse = try await get(path: "/chains")
        return response.chains ?? []
    }

    /// Get chain details.
    public func getChain(chainId: ChainId) async throws -> Chain {
        return try await get(path: "/chains/\(chainId.rawValue)")
    }

    /// Check if a chain is supported.
    public func isChainSupported(chainId: ChainId) async -> Bool {
        do {
            let chain = try await getChain(chainId: chainId)
            return chain.supported
        } catch {
            return false
        }
    }

    // MARK: - Asset Operations

    /// Get supported assets for a chain.
    public func getSupportedAssets(chainId: ChainId) async throws -> [Asset] {
        let response: AssetsResponse = try await get(path: "/chains/\(chainId.rawValue)/assets")
        return response.assets ?? []
    }

    /// Get asset details.
    public func getAsset(assetId: String) async throws -> Asset {
        return try await get(path: "/assets/\(assetId.urlEncoded)")
    }

    /// Get wrapped asset on a target chain.
    public func getWrappedAsset(originalAssetId: String, targetChain: ChainId) async throws -> WrappedAsset {
        return try await get(path: "/assets/\(originalAssetId.urlEncoded)/wrapped/\(targetChain.rawValue)")
    }

    // MARK: - Fee & Rate Operations

    /// Estimate transfer fee.
    public func estimateFee(
        asset: String,
        amount: String,
        sourceChain: ChainId,
        targetChain: ChainId
    ) async throws -> BridgeFeeEstimate {
        let body: [String: String] = [
            "asset": asset,
            "amount": amount,
            "sourceChain": sourceChain.rawValue,
            "targetChain": targetChain.rawValue
        ]
        return try await post(path: "/fees/estimate", body: body)
    }

    /// Get exchange rate between assets.
    public func getExchangeRate(fromAsset: String, toAsset: String) async throws -> ExchangeRate {
        return try await get(path: "/rates/\(fromAsset.urlEncoded)/\(toAsset.urlEncoded)")
    }

    // MARK: - Lock-Mint Flow

    /// Lock assets on the source chain.
    public func lock(
        asset: String,
        amount: String,
        targetChain: ChainId,
        options: LockOptions? = nil
    ) async throws -> LockReceipt {
        var body: [String: Any] = [
            "asset": asset,
            "amount": amount,
            "targetChain": targetChain.rawValue
        ]
        if let recipient = options?.recipient { body["recipient"] = recipient }
        if let deadline = options?.deadline { body["deadline"] = deadline }
        if let slippage = options?.slippage { body["slippage"] = slippage }
        return try await post(path: "/transfers/lock", body: body)
    }

    /// Get lock proof.
    public func getLockProof(lockReceiptId: String) async throws -> LockProof {
        return try await get(path: "/transfers/lock/\(lockReceiptId.urlEncoded)/proof")
    }

    /// Wait for lock proof with confirmations.
    public func waitForLockProof(
        lockReceiptId: String,
        pollInterval: TimeInterval = 5,
        maxWait: TimeInterval = 600
    ) async throws -> LockProof {
        let deadline = Date().addingTimeInterval(maxWait)

        while Date() < deadline {
            do {
                return try await getLockProof(lockReceiptId: lockReceiptId)
            } catch let error as BridgeError {
                if error.isConfirmationsPending {
                    try await Task.sleep(nanoseconds: UInt64(pollInterval * 1_000_000_000))
                    continue
                }
                throw error
            }
        }

        throw BridgeError.timeout("Waiting for lock proof")
    }

    /// Mint assets on the target chain.
    public func mint(
        proof: LockProof,
        targetAddress: String,
        options: MintOptions? = nil
    ) async throws -> BridgeSignedTransaction {
        var body: [String: Any] = [
            "proof": try proofToDictionary(proof),
            "targetAddress": targetAddress
        ]
        if let gasLimit = options?.gasLimit { body["gasLimit"] = gasLimit }
        if let maxFeePerGas = options?.maxFeePerGas { body["maxFeePerGas"] = maxFeePerGas }
        if let maxPriorityFeePerGas = options?.maxPriorityFeePerGas { body["maxPriorityFeePerGas"] = maxPriorityFeePerGas }
        return try await post(path: "/transfers/mint", body: body)
    }

    // MARK: - Burn-Unlock Flow

    /// Burn wrapped assets.
    public func burn(
        wrappedAsset: String,
        amount: String,
        options: BurnOptions? = nil
    ) async throws -> BurnReceipt {
        var body: [String: Any] = [
            "wrappedAsset": wrappedAsset,
            "amount": amount
        ]
        if let recipient = options?.recipient { body["recipient"] = recipient }
        if let deadline = options?.deadline { body["deadline"] = deadline }
        return try await post(path: "/transfers/burn", body: body)
    }

    /// Get burn proof.
    public func getBurnProof(burnReceiptId: String) async throws -> BurnProof {
        return try await get(path: "/transfers/burn/\(burnReceiptId.urlEncoded)/proof")
    }

    /// Wait for burn proof with confirmations.
    public func waitForBurnProof(
        burnReceiptId: String,
        pollInterval: TimeInterval = 5,
        maxWait: TimeInterval = 600
    ) async throws -> BurnProof {
        let deadline = Date().addingTimeInterval(maxWait)

        while Date() < deadline {
            do {
                return try await getBurnProof(burnReceiptId: burnReceiptId)
            } catch let error as BridgeError {
                if error.isConfirmationsPending {
                    try await Task.sleep(nanoseconds: UInt64(pollInterval * 1_000_000_000))
                    continue
                }
                throw error
            }
        }

        throw BridgeError.timeout("Waiting for burn proof")
    }

    /// Unlock original assets.
    public func unlock(
        proof: BurnProof,
        options: UnlockOptions? = nil
    ) async throws -> BridgeSignedTransaction {
        var body: [String: Any] = [
            "proof": try burnProofToDictionary(proof)
        ]
        if let gasLimit = options?.gasLimit { body["gasLimit"] = gasLimit }
        if let gasPrice = options?.gasPrice { body["gasPrice"] = gasPrice }
        return try await post(path: "/transfers/unlock", body: body)
    }

    // MARK: - Transfer Management

    /// Get transfer details.
    public func getTransfer(transferId: String) async throws -> Transfer {
        return try await get(path: "/transfers/\(transferId.urlEncoded)")
    }

    /// Get transfer status.
    public func getTransferStatus(transferId: String) async throws -> TransferStatus {
        let transfer = try await getTransfer(transferId: transferId)
        return transfer.status
    }

    /// List transfers with optional filter.
    public func listTransfers(filter: TransferFilter? = nil) async throws -> [Transfer] {
        var params: [String] = []
        if let status = filter?.status { params.append("status=\(status.rawValue)") }
        if let sourceChain = filter?.sourceChain { params.append("sourceChain=\(sourceChain.rawValue)") }
        if let targetChain = filter?.targetChain { params.append("targetChain=\(targetChain.rawValue)") }
        if let limit = filter?.limit { params.append("limit=\(limit)") }
        if let offset = filter?.offset { params.append("offset=\(offset)") }

        var path = "/transfers"
        if !params.isEmpty {
            path += "?\(params.joined(separator: "&"))"
        }

        let response: TransfersResponse = try await get(path: path)
        return response.transfers ?? []
    }

    /// Wait for transfer to complete.
    public func waitForTransfer(
        transferId: String,
        pollInterval: TimeInterval = 10,
        maxWait: TimeInterval = 1800
    ) async throws -> Transfer {
        let deadline = Date().addingTimeInterval(maxWait)

        while Date() < deadline {
            let transfer = try await getTransfer(transferId: transferId)
            if Self.finalStatuses.contains(transfer.status) {
                return transfer
            }
            try await Task.sleep(nanoseconds: UInt64(pollInterval * 1_000_000_000))
        }

        throw BridgeError.timeout("Waiting for transfer completion")
    }

    // MARK: - Convenience Methods

    /// Complete lock-mint bridge flow.
    public func bridgeTo(
        asset: String,
        amount: String,
        targetChain: ChainId,
        targetAddress: String,
        lockOptions: LockOptions? = nil,
        mintOptions: MintOptions? = nil
    ) async throws -> Transfer {
        let lockReceipt = try await lock(asset: asset, amount: amount, targetChain: targetChain, options: lockOptions)
        if config.debug {
            print("Locked: \(lockReceipt.id), waiting for confirmations...")
        }

        let proof = try await waitForLockProof(lockReceiptId: lockReceipt.id)
        if config.debug {
            print("Proof ready, minting on \(targetChain.rawValue)...")
        }

        _ = try await mint(proof: proof, targetAddress: targetAddress, options: mintOptions)
        return try await waitForTransfer(transferId: lockReceipt.id)
    }

    /// Complete burn-unlock bridge flow.
    public func bridgeBack(
        wrappedAsset: String,
        amount: String,
        burnOptions: BurnOptions? = nil,
        unlockOptions: UnlockOptions? = nil
    ) async throws -> Transfer {
        let burnReceipt = try await burn(wrappedAsset: wrappedAsset, amount: amount, options: burnOptions)
        if config.debug {
            print("Burned: \(burnReceipt.id), waiting for confirmations...")
        }

        let proof = try await waitForBurnProof(burnReceiptId: burnReceipt.id)
        if config.debug {
            print("Proof ready, unlocking on \(burnReceipt.targetChain.rawValue)...")
        }

        _ = try await unlock(proof: proof, options: unlockOptions)
        return try await waitForTransfer(transferId: burnReceipt.id)
    }

    // MARK: - Lifecycle

    /// Close the client.
    public func close() {
        closed = true
        session.invalidateAndCancel()
    }

    /// Check if the client is closed.
    public var isClosed: Bool { closed }

    /// Perform a health check.
    public func healthCheck() async -> Bool {
        do {
            let response: BridgeHealthResponse = try await get(path: "/health")
            return response.status == "healthy"
        } catch {
            return false
        }
    }

    // MARK: - Private 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 [String: Any]?)
        }
    }

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

    private func performRequest<T: Decodable>(
        method: String,
        path: String,
        body: [String: Any]?
    ) async throws -> T {
        if closed { throw BridgeError.clientClosed }

        guard let url = URL(string: "\(config.endpoint)\(path)") else {
            throw BridgeError.invalidResponse
        }

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

        if let body = body {
            do {
                request.httpBody = try JSONSerialization.data(withJSONObject: body)
            } catch {
                throw BridgeError.encodingError(error)
            }
        }

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

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

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

        let errorInfo = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
        let message = errorInfo?["message"] as? String ?? "HTTP \(httpResponse.statusCode)"
        let code = errorInfo?["code"] as? String
        throw BridgeError.httpError(statusCode: httpResponse.statusCode, message: message, code: code)
    }

    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 * (1 << attempt)))
                }
            }
        }

        throw lastError ?? BridgeError.invalidResponse
    }

    // MARK: - Private Helpers

    private func proofToDictionary(_ proof: LockProof) throws -> [String: Any] {
        let data = try encoder.encode(proof)
        guard let dict = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
            throw BridgeError.encodingError(NSError(domain: "BridgeError", code: 0, userInfo: [NSLocalizedDescriptionKey: "Failed to encode proof"]))
        }
        return dict
    }

    private func burnProofToDictionary(_ proof: BurnProof) throws -> [String: Any] {
        let data = try encoder.encode(proof)
        guard let dict = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
            throw BridgeError.encodingError(NSError(domain: "BridgeError", code: 0, userInfo: [NSLocalizedDescriptionKey: "Failed to encode proof"]))
        }
        return dict
    }
}

// MARK: - Private Helpers

private extension String {
    var urlEncoded: String {
        addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? self
    }
}
