import Foundation

/// Synor Contract SDK client for Swift.
/// Smart contract deployment, interaction, and event handling.
public actor ContractClient {
    public static let version = "0.1.0"

    private let config: ContractConfig
    private let session: URLSession
    private var isClosed = false

    public init(config: ContractConfig) {
        self.config = config
        let sessionConfig = URLSessionConfiguration.default
        sessionConfig.timeoutIntervalForRequest = TimeInterval(config.timeoutMs) / 1000.0
        self.session = URLSession(configuration: sessionConfig)
    }

    // MARK: - Contract Deployment

    public func deploy(_ options: DeployContractOptions) async throws -> DeploymentResult {
        var body: [String: Any] = ["bytecode": options.bytecode]
        if let abi = options.abi { body["abi"] = abi.map { $0.toDictionary() } }
        if let args = options.constructorArgs { body["constructor_args"] = args }
        if let value = options.value { body["value"] = value }
        if let gasLimit = options.gasLimit { body["gas_limit"] = gasLimit }
        if let gasPrice = options.gasPrice { body["gas_price"] = gasPrice }
        if let nonce = options.nonce { body["nonce"] = nonce }
        return try await post("/contract/deploy", body: body)
    }

    public func deployCreate2(_ options: DeployContractOptions, salt: String) async throws -> DeploymentResult {
        var body: [String: Any] = ["bytecode": options.bytecode, "salt": salt]
        if let abi = options.abi { body["abi"] = abi.map { $0.toDictionary() } }
        if let args = options.constructorArgs { body["constructor_args"] = args }
        if let value = options.value { body["value"] = value }
        if let gasLimit = options.gasLimit { body["gas_limit"] = gasLimit }
        if let gasPrice = options.gasPrice { body["gas_price"] = gasPrice }
        return try await post("/contract/deploy/create2", body: body)
    }

    public func predictAddress(bytecode: String, salt: String, deployer: String? = nil) async throws -> String {
        var body: [String: Any] = ["bytecode": bytecode, "salt": salt]
        if let deployer = deployer { body["deployer"] = deployer }
        let response: [String: Any] = try await post("/contract/predict-address", body: body)
        guard let address = response["address"] as? String else {
            throw ContractError.response("Missing address")
        }
        return address
    }

    // MARK: - Contract Interaction

    public func call(_ options: CallContractOptions) async throws -> Any {
        let body: [String: Any] = [
            "contract": options.contract,
            "method": options.method,
            "args": options.args,
            "abi": options.abi.map { $0.toDictionary() }
        ]
        return try await post("/contract/call", body: body) as [String: Any]
    }

    public func send(_ options: SendContractOptions) async throws -> TransactionResult {
        var body: [String: Any] = [
            "contract": options.contract,
            "method": options.method,
            "args": options.args,
            "abi": options.abi.map { $0.toDictionary() }
        ]
        if let value = options.value { body["value"] = value }
        if let gasLimit = options.gasLimit { body["gas_limit"] = gasLimit }
        if let gasPrice = options.gasPrice { body["gas_price"] = gasPrice }
        if let nonce = options.nonce { body["nonce"] = nonce }
        return try await post("/contract/send", body: body)
    }

    // MARK: - Events

    public func getEvents(_ filter: EventFilter) async throws -> [DecodedEvent] {
        var body: [String: Any] = ["contract": filter.contract]
        if let event = filter.event { body["event"] = event }
        if let fromBlock = filter.fromBlock { body["from_block"] = fromBlock }
        if let toBlock = filter.toBlock { body["to_block"] = toBlock }
        if let topics = filter.topics { body["topics"] = topics }
        if let abi = filter.abi { body["abi"] = abi.map { $0.toDictionary() } }
        return try await post("/contract/events", body: body)
    }

    public func getLogs(contract: String, fromBlock: Int64? = nil, toBlock: Int64? = nil) async throws -> [EventLog] {
        var path = "/contract/logs?contract=\(contract.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? contract)"
        if let fromBlock = fromBlock { path += "&from_block=\(fromBlock)" }
        if let toBlock = toBlock { path += "&to_block=\(toBlock)" }
        return try await get(path)
    }

    public func decodeLogs(_ logs: [EventLog], abi: [AbiEntry]) async throws -> [DecodedEvent] {
        let body: [String: Any] = [
            "logs": logs.map { $0.toDictionary() },
            "abi": abi.map { $0.toDictionary() }
        ]
        return try await post("/contract/decode-logs", body: body)
    }

    // MARK: - ABI Utilities

    public func encodeCall(_ options: EncodeCallOptions) async throws -> String {
        let body: [String: Any] = [
            "method": options.method,
            "args": options.args,
            "abi": options.abi.map { $0.toDictionary() }
        ]
        let response: [String: Any] = try await post("/contract/encode", body: body)
        guard let data = response["data"] as? String else {
            throw ContractError.response("Missing data")
        }
        return data
    }

    public func decodeResult(_ options: DecodeResultOptions) async throws -> Any {
        let body: [String: Any] = [
            "data": options.data,
            "method": options.method,
            "abi": options.abi.map { $0.toDictionary() }
        ]
        let response: [String: Any] = try await post("/contract/decode", body: body)
        return response["result"] as Any
    }

    public func getSelector(_ signature: String) async throws -> String {
        let encoded = signature.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? signature
        let response: [String: Any] = try await get("/contract/selector?signature=\(encoded)")
        guard let selector = response["selector"] as? String else {
            throw ContractError.response("Missing selector")
        }
        return selector
    }

    // MARK: - Gas Estimation

    public func estimateGas(_ options: EstimateGasOptions) async throws -> GasEstimation {
        var body: [String: Any] = [
            "contract": options.contract,
            "method": options.method,
            "args": options.args,
            "abi": options.abi.map { $0.toDictionary() }
        ]
        if let value = options.value { body["value"] = value }
        return try await post("/contract/estimate-gas", body: body)
    }

    // MARK: - Contract Information

    public func getBytecode(_ address: String) async throws -> BytecodeInfo {
        let encoded = address.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? address
        return try await get("/contract/\(encoded)/bytecode")
    }

    public func verify(_ options: VerifyContractOptions) async throws -> VerificationResult {
        var body: [String: Any] = [
            "address": options.address,
            "source_code": options.sourceCode,
            "compiler_version": options.compilerVersion
        ]
        if let args = options.constructorArgs { body["constructor_args"] = args }
        if let optimization = options.optimization { body["optimization"] = optimization }
        if let runs = options.optimizationRuns { body["optimization_runs"] = runs }
        if let license = options.license { body["license"] = license }
        return try await post("/contract/verify", body: body)
    }

    public func getVerificationStatus(_ address: String) async throws -> VerificationResult {
        let encoded = address.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? address
        return try await get("/contract/\(encoded)/verification")
    }

    // MARK: - Multicall

    public func multicall(_ requests: [MulticallRequest]) async throws -> [MulticallResult] {
        let body: [String: Any] = ["calls": requests.map { $0.toDictionary() }]
        return try await post("/contract/multicall", body: body)
    }

    // MARK: - Storage

    public func readStorage(_ options: ReadStorageOptions) async throws -> String {
        let contract = options.contract.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? options.contract
        let slot = options.slot.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? options.slot
        var path = "/contract/storage?contract=\(contract)&slot=\(slot)"
        if let block = options.blockNumber { path += "&block=\(block)" }
        let response: [String: Any] = try await get(path)
        guard let value = response["value"] as? String else {
            throw ContractError.response("Missing value")
        }
        return value
    }

    // MARK: - Lifecycle

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

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

    // MARK: - HTTP Helpers

    private func get<T: Decodable>(_ path: String) async throws -> T {
        try checkClosed()
        return try await executeWithRetry {
            let url = URL(string: "\(self.config.endpoint)\(path)")!
            var request = URLRequest(url: url)
            request.httpMethod = "GET"
            self.addHeaders(to: &request)
            return try await self.execute(request)
        }
    }

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

    private func addHeaders(to request: inout URLRequest) {
        request.setValue("Bearer \(config.apiKey)", forHTTPHeaderField: "Authorization")
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.setValue("swift/\(Self.version)", forHTTPHeaderField: "X-SDK-Version")
    }

    private func execute<T: Decodable>(_ request: URLRequest) async throws -> T {
        let (data, response) = try await session.data(for: request)

        guard let httpResponse = response as? HTTPURLResponse else {
            throw ContractError.request("Invalid response")
        }

        if httpResponse.statusCode >= 200 && httpResponse.statusCode < 300 {
            return try JSONDecoder().decode(T.self, from: data)
        }

        if let errorJson = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
            let message = errorJson["message"] as? String ?? errorJson["error"] as? String ?? "Unknown error"
            let code = errorJson["code"] as? String
            throw ContractError.api(message: message, code: code, statusCode: httpResponse.statusCode)
        }

        throw ContractError.response("HTTP \(httpResponse.statusCode)")
    }

    private func executeWithRetry<T>(_ block: () async throws -> T) async throws -> T {
        var lastError: Error?
        for attempt in 0..<config.retries {
            do {
                return try await block()
            } catch {
                lastError = error
                if attempt < config.retries - 1 {
                    try await Task.sleep(nanoseconds: UInt64(pow(2.0, Double(attempt))) * 1_000_000_000)
                }
            }
        }
        throw lastError ?? ContractError.request("Request failed")
    }

    private func checkClosed() throws {
        if isClosed {
            throw ContractError.closed
        }
    }
}

/// Contract SDK configuration.
public struct ContractConfig {
    public let apiKey: String
    public var endpoint: String
    public var timeoutMs: Int
    public var retries: Int

    public init(
        apiKey: String,
        endpoint: String = "https://contract.synor.io",
        timeoutMs: Int = 30000,
        retries: Int = 3
    ) {
        self.apiKey = apiKey
        self.endpoint = endpoint
        self.timeoutMs = timeoutMs
        self.retries = retries
    }
}

/// Contract SDK errors.
public enum ContractError: Error {
    case request(String)
    case response(String)
    case api(message: String, code: String?, statusCode: Int)
    case closed
}
