import Foundation

/// Synor DEX SDK Client
///
/// Complete decentralized exchange client with support for:
/// - AMM swaps (constant product, stable, concentrated)
/// - Liquidity provision
/// - Perpetual futures (up to 100x leverage)
/// - Order books (limit orders)
/// - Farming & staking
public class SynorDex {
    private let config: DexConfig
    private let session: URLSession
    private var closed = false

    public let perps: PerpsClient
    public let orderbook: OrderBookClient
    public let farms: FarmsClient

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

        let configuration = URLSessionConfiguration.default
        configuration.timeoutIntervalForRequest = TimeInterval(config.timeout) / 1000
        self.session = URLSession(configuration: configuration)

        self.perps = PerpsClient()
        self.orderbook = OrderBookClient()
        self.farms = FarmsClient()

        self.perps.dex = self
        self.orderbook.dex = self
        self.farms.dex = self
    }

    // MARK: - Token Operations

    public func getToken(address: String) async throws -> Token {
        try await get("/tokens/\(address)")
    }

    public func listTokens() async throws -> [Token] {
        try await get("/tokens")
    }

    public func searchTokens(query: String) async throws -> [Token] {
        let encoded = query.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? query
        return try await get("/tokens/search?q=\(encoded)")
    }

    // MARK: - Pool Operations

    public func getPool(tokenA: String, tokenB: String) async throws -> Pool {
        try await get("/pools/\(tokenA)/\(tokenB)")
    }

    public func getPoolById(poolId: String) async throws -> Pool {
        try await get("/pools/\(poolId)")
    }

    public func listPools(filter: PoolFilter? = nil) async throws -> [Pool] {
        var params: [String] = []
        if let f = filter {
            if let tokens = f.tokens { params.append("tokens=\(tokens.joined(separator: ","))") }
            if let minTvl = f.minTvl { params.append("min_tvl=\(minTvl)") }
            if let minVolume = f.minVolume24h { params.append("min_volume=\(minVolume)") }
            if let verified = f.verified { params.append("verified=\(verified)") }
            if let limit = f.limit { params.append("limit=\(limit)") }
            if let offset = f.offset { params.append("offset=\(offset)") }
        }
        let path = params.isEmpty ? "/pools" : "/pools?\(params.joined(separator: "&"))"
        return try await get(path)
    }

    // MARK: - Swap Operations

    public func getQuote(params: QuoteParams) async throws -> Quote {
        try await post("/swap/quote", body: [
            "token_in": params.tokenIn,
            "token_out": params.tokenOut,
            "amount_in": String(params.amountIn),
            "slippage": params.slippage ?? 0.005
        ])
    }

    public func swap(params: SwapParams) async throws -> SwapResult {
        let deadline = params.deadline ?? Int(Date().timeIntervalSince1970) + 1200
        var body: [String: Any] = [
            "token_in": params.tokenIn,
            "token_out": params.tokenOut,
            "amount_in": String(params.amountIn),
            "min_amount_out": String(params.minAmountOut),
            "deadline": deadline
        ]
        if let recipient = params.recipient {
            body["recipient"] = recipient
        }
        return try await post("/swap", body: body)
    }

    // MARK: - Liquidity Operations

    public func addLiquidity(params: AddLiquidityParams) async throws -> LiquidityResult {
        let deadline = params.deadline ?? Int(Date().timeIntervalSince1970) + 1200
        var body: [String: Any] = [
            "token_a": params.tokenA,
            "token_b": params.tokenB,
            "amount_a": String(params.amountA),
            "amount_b": String(params.amountB),
            "deadline": deadline
        ]
        if let minA = params.minAmountA { body["min_amount_a"] = String(minA) }
        if let minB = params.minAmountB { body["min_amount_b"] = String(minB) }
        return try await post("/liquidity/add", body: body)
    }

    public func removeLiquidity(params: RemoveLiquidityParams) async throws -> LiquidityResult {
        let deadline = params.deadline ?? Int(Date().timeIntervalSince1970) + 1200
        var body: [String: Any] = [
            "pool": params.pool,
            "lp_amount": String(params.lpAmount),
            "deadline": deadline
        ]
        if let minA = params.minAmountA { body["min_amount_a"] = String(minA) }
        if let minB = params.minAmountB { body["min_amount_b"] = String(minB) }
        return try await post("/liquidity/remove", body: body)
    }

    public func getMyPositions() async throws -> [LPPosition] {
        try await get("/liquidity/positions")
    }

    // MARK: - Analytics

    public func getPriceHistory(pair: String, interval: String, limit: Int = 100) async throws -> [OHLCV] {
        try await get("/analytics/candles/\(pair)?interval=\(interval)&limit=\(limit)")
    }

    public func getTradeHistory(pair: String, limit: Int = 50) async throws -> [TradeHistory] {
        try await get("/analytics/trades/\(pair)?limit=\(limit)")
    }

    public func getVolumeStats() async throws -> VolumeStats {
        try await get("/analytics/volume")
    }

    public func getTVL() async throws -> TVLStats {
        try await get("/analytics/tvl")
    }

    // MARK: - Lifecycle

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

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

    public var isClosed: Bool { closed }

    // MARK: - Internal Methods

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

    func post<T: Decodable>(_ path: String, body: [String: Any]) async throws -> T {
        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 execute(request)
    }

    func delete<T: Decodable>(_ path: String) async throws -> T {
        try checkClosed()
        var request = URLRequest(url: URL(string: config.endpoint + path)!)
        request.httpMethod = "DELETE"
        addHeaders(&request)
        return try await execute(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")
    }

    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 DexError.network("Invalid response")
        }

        guard (200...299).contains(httpResponse.statusCode) else {
            let error = try? JSONDecoder().decode(ErrorResponse.self, from: data)
            throw DexError.http(
                message: error?.message ?? "HTTP \(httpResponse.statusCode)",
                code: error?.code,
                status: httpResponse.statusCode
            )
        }

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

    private func checkClosed() throws {
        if closed {
            throw DexError.clientClosed
        }
    }

    private struct HealthResponse: Decodable {
        let status: String
    }

    private struct ErrorResponse: Decodable {
        let message: String?
        let code: String?
    }
}

/// DEX client configuration
public struct DexConfig {
    public let apiKey: String
    public let endpoint: String
    public let wsEndpoint: String
    public let timeout: Int
    public let retries: Int
    public let debug: Bool

    public init(
        apiKey: String,
        endpoint: String = "https://dex.synor.io/v1",
        wsEndpoint: String = "wss://dex.synor.io/v1/ws",
        timeout: Int = 30000,
        retries: Int = 3,
        debug: Bool = false
    ) {
        self.apiKey = apiKey
        self.endpoint = endpoint
        self.wsEndpoint = wsEndpoint
        self.timeout = timeout
        self.retries = retries
        self.debug = debug
    }
}

/// DEX SDK Error
public enum DexError: Error {
    case http(message: String, code: String?, status: Int)
    case network(String)
    case clientClosed
}

/// Perpetual futures sub-client
public class PerpsClient {
    weak var dex: SynorDex?

    public func listMarkets() async throws -> [PerpMarket] {
        try await dex!.get("/perps/markets")
    }

    public func getMarket(symbol: String) async throws -> PerpMarket {
        try await dex!.get("/perps/markets/\(symbol)")
    }

    public func openPosition(params: OpenPositionParams) async throws -> PerpPosition {
        var body: [String: Any] = [
            "market": params.market,
            "side": params.side.rawValue,
            "size": String(params.size),
            "leverage": params.leverage,
            "order_type": params.orderType.rawValue,
            "margin_type": params.marginType.rawValue,
            "reduce_only": params.reduceOnly
        ]
        if let price = params.limitPrice { body["limit_price"] = price }
        if let sl = params.stopLoss { body["stop_loss"] = sl }
        if let tp = params.takeProfit { body["take_profit"] = tp }
        return try await dex!.post("/perps/positions", body: body)
    }

    public func closePosition(params: ClosePositionParams) async throws -> PerpPosition {
        var body: [String: Any] = [
            "market": params.market,
            "order_type": params.orderType.rawValue
        ]
        if let size = params.size { body["size"] = String(size) }
        if let price = params.limitPrice { body["limit_price"] = price }
        return try await dex!.post("/perps/positions/close", body: body)
    }

    public func getPositions() async throws -> [PerpPosition] {
        try await dex!.get("/perps/positions")
    }

    public func getOrders() async throws -> [PerpOrder] {
        try await dex!.get("/perps/orders")
    }

    public func getFundingHistory(market: String, limit: Int = 100) async throws -> [FundingPayment] {
        try await dex!.get("/perps/funding/\(market)?limit=\(limit)")
    }
}

/// Order book sub-client
public class OrderBookClient {
    weak var dex: SynorDex?

    public func getOrderBook(market: String, depth: Int = 20) async throws -> OrderBook {
        try await dex!.get("/orderbook/\(market)?depth=\(depth)")
    }

    public func placeLimitOrder(params: LimitOrderParams) async throws -> Order {
        try await dex!.post("/orderbook/orders", body: [
            "market": params.market,
            "side": params.side,
            "price": params.price,
            "size": String(params.size),
            "time_in_force": params.timeInForce.rawValue,
            "post_only": params.postOnly
        ])
    }

    public func getOpenOrders(market: String? = nil) async throws -> [Order] {
        let path = market.map { "/orderbook/orders?market=\($0)" } ?? "/orderbook/orders"
        return try await dex!.get(path)
    }
}

/// Farms sub-client
public class FarmsClient {
    weak var dex: SynorDex?

    public func listFarms() async throws -> [Farm] {
        try await dex!.get("/farms")
    }

    public func getFarm(farmId: String) async throws -> Farm {
        try await dex!.get("/farms/\(farmId)")
    }

    public func stake(params: StakeParams) async throws -> FarmPosition {
        try await dex!.post("/farms/stake", body: [
            "farm": params.farm,
            "amount": String(params.amount)
        ])
    }

    public func getMyFarmPositions() async throws -> [FarmPosition] {
        try await dex!.get("/farms/positions")
    }
}
