import Foundation

/// Synor Economics SDK client for Swift.
///
/// Provides pricing, billing, staking, and discount operations.
///
/// Example:
/// ```swift
/// let economics = SynorEconomics(config: EconomicsConfig(apiKey: "your-api-key"))
///
/// // Get price for compute usage
/// let usage = UsageMetrics(computeHours: 100, gpuHours: 10)
/// let price = try await economics.getPrice(service: .compute, usage: usage)
///
/// // Stake tokens
/// let receipt = try await economics.stake(amount: "1000000000000000000")
/// ```
public class SynorEconomics {
    private let config: EconomicsConfig
    private let session: URLSession
    private let decoder: JSONDecoder
    private let encoder: JSONEncoder
    private var closed = false

    public init(config: EconomicsConfig) {
        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: - Pricing Operations

    /// Get price for service usage.
    public func getPrice(service: ServiceType, usage: UsageMetrics) async throws -> Price {
        let body: [String: Any] = [
            "service": service.rawValue,
            "usage": [
                "computeHours": usage.computeHours,
                "storageGb": usage.storageGb,
                "requests": usage.requests,
                "bandwidthGb": usage.bandwidthGb,
                "gpuHours": usage.gpuHours
            ]
        ]
        return try await post(path: "/pricing/calculate", body: body)
    }

    /// Estimate cost for a usage plan.
    public func estimateCost(plan: UsagePlan) async throws -> CostEstimate {
        let body: [String: Any] = [
            "service": plan.service.rawValue,
            "estimatedUsage": [
                "computeHours": plan.estimatedUsage.computeHours,
                "storageGb": plan.estimatedUsage.storageGb,
                "requests": plan.estimatedUsage.requests,
                "bandwidthGb": plan.estimatedUsage.bandwidthGb,
                "gpuHours": plan.estimatedUsage.gpuHours
            ],
            "period": plan.period.rawValue
        ]
        return try await post(path: "/pricing/estimate", body: body)
    }

    /// Get pricing tiers for a service.
    public func getPricingTiers(service: ServiceType) async throws -> [PricingTier] {
        let response: TiersResponse = try await get(path: "/pricing/tiers/\(service.rawValue)")
        return response.tiers ?? []
    }

    // MARK: - Billing Operations

    /// Get usage for a billing period.
    public func getUsage(period: BillingPeriod? = nil) async throws -> Usage {
        var path = "/billing/usage"
        if let period = period {
            path += "?period=\(period.rawValue)"
        }
        return try await get(path: path)
    }

    /// Get usage by date range.
    public func getUsageByDateRange(startDate: Int64, endDate: Int64) async throws -> Usage {
        return try await get(path: "/billing/usage?startDate=\(startDate)&endDate=\(endDate)")
    }

    /// Get all invoices.
    public func getInvoices() async throws -> [Invoice] {
        let response: InvoicesResponse = try await get(path: "/billing/invoices")
        return response.invoices ?? []
    }

    /// Get invoice by ID.
    public func getInvoice(invoiceId: String) async throws -> Invoice {
        return try await get(path: "/billing/invoices/\(invoiceId.urlEncoded)")
    }

    /// Download invoice as PDF data.
    public func downloadInvoice(invoiceId: String) async throws -> Data {
        let response: [String: String] = try await get(path: "/billing/invoices/\(invoiceId.urlEncoded)/pdf")
        guard let base64 = response["data"],
              let data = Data(base64Encoded: base64) else {
            throw EconomicsError(message: "Invalid PDF data")
        }
        return data
    }

    /// Get account balance.
    public func getBalance() async throws -> AccountBalance {
        return try await get(path: "/billing/balance")
    }

    /// Add credits to account.
    public func addCredits(amount: String, paymentMethod: String) async throws {
        let body = ["amount": amount, "paymentMethod": paymentMethod]
        let _: [String: Any] = try await post(path: "/billing/credits", body: body)
    }

    // MARK: - Staking Operations

    /// Stake tokens.
    public func stake(amount: String, durationDays: Int? = nil) async throws -> StakeReceipt {
        var body: [String: Any] = ["amount": amount]
        if let duration = durationDays { body["durationDays"] = duration }
        return try await post(path: "/staking/stake", body: body)
    }

    /// Unstake tokens.
    public func unstake(stakeId: String) async throws -> UnstakeReceipt {
        return try await post(path: "/staking/unstake/\(stakeId.urlEncoded)", body: [:] as [String: String])
    }

    /// Get all stakes.
    public func getStakes() async throws -> [StakeInfo] {
        let response: StakesResponse = try await get(path: "/staking/stakes")
        return response.stakes ?? []
    }

    /// Get stake by ID.
    public func getStake(stakeId: String) async throws -> StakeInfo {
        return try await get(path: "/staking/stakes/\(stakeId.urlEncoded)")
    }

    /// Get staking rewards.
    public func getStakingRewards() async throws -> Rewards {
        return try await get(path: "/staking/rewards")
    }

    /// Claim staking rewards.
    public func claimRewards() async throws -> String {
        let response: [String: String] = try await post(path: "/staking/rewards/claim", body: [:] as [String: String])
        return response["txHash"] ?? ""
    }

    /// Get current APY.
    public func getCurrentApy() async throws -> Double {
        let response: [String: Double] = try await get(path: "/staking/apy")
        return response["apy"] ?? 0
    }

    // MARK: - Discount Operations

    /// Apply a discount code.
    public func applyDiscount(code: String) async throws -> AppliedDiscount {
        return try await post(path: "/discounts/apply", body: ["code": code])
    }

    /// Get available discounts.
    public func getAvailableDiscounts() async throws -> [Discount] {
        let response: DiscountsResponse = try await get(path: "/discounts/available")
        return response.discounts ?? []
    }

    /// Validate a discount code.
    public func validateDiscount(code: String) async throws -> Discount {
        return try await get(path: "/discounts/validate/\(code.urlEncoded)")
    }

    /// Get applied discounts.
    public func getAppliedDiscounts() async throws -> [AppliedDiscount] {
        let response: AppliedDiscountsResponse = try await get(path: "/discounts/applied")
        return response.discounts ?? []
    }

    /// Remove a discount.
    public func removeDiscount(discountId: String) async throws {
        let _: [String: Any] = try await delete(path: "/discounts/\(discountId.urlEncoded)")
    }

    // MARK: - Lifecycle

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

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

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

    // MARK: - Private Methods

    private func get<T: Decodable>(path: String) async throws -> T {
        return try await request(method: "GET", path: path)
    }

    private func post<T: Decodable>(path: String, body: Any) async throws -> T {
        return try await request(method: "POST", path: path, body: body)
    }

    private func delete<T: Decodable>(path: String) async throws -> T {
        return try await request(method: "DELETE", path: path)
    }

    private func request<T: Decodable>(method: String, path: String, body: Any? = nil) async throws -> T {
        guard !closed else { throw EconomicsError(message: "Client has been closed") }

        var lastError: Error?
        for attempt in 0..<config.retries {
            do {
                return try await doRequest(method: method, path: path, body: body)
            } 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 ?? EconomicsError(message: "Unknown error")
    }

    private func doRequest<T: Decodable>(method: String, path: String, body: Any?) async throws -> T {
        guard let url = URL(string: config.endpoint + path) else {
            throw EconomicsError(message: "Invalid URL")
        }

        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 {
            request.httpBody = try JSONSerialization.data(withJSONObject: body)
        }

        let (data, response) = try await session.data(for: request)

        guard let httpResponse = response as? HTTPURLResponse else {
            throw EconomicsError(message: "Invalid response")
        }

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

        return try decoder.decode(T.self, from: data)
    }
}

// MARK: - Extensions

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