import Foundation

/**
 * Custom S2S bid request client
 *
 * Replaces Prebid SDK's `fetchDemand()` with a direct HTTP call to the BigCrunch
 * S2S endpoint. Builds OpenRTB-style requests with `ext.bidders` format and
 * implements screen-level batching (100ms debounce) to combine multiple placements
 * into a single multi-impression request.
 *
 * Usage:
 * ```swift
 * let bidResponse = await bidRequestClient.fetchDemand(placement: placement)
 * // bidResponse contains targeting KVPs and auction metadata
 * ```
 */
internal class BidRequestClient {

    private let httpClient: HTTPClient
    private let configManager: ConfigManager
    private let privacyStore: PrivacyStore
    private let s2sConfig: S2SConfig

    private let lock = NSLock()
    private var pendingRequests: [PendingRequest] = []
    private var batchTimer: Timer?

    private static let batchDelayMs: Int = 100

    // MARK: - Types

    /// Bid response containing both targeting KVPs and auction metadata
    internal struct BidResponse {
        let targeting: [String: String]
        let auctionData: AuctionData
    }

    private struct PendingRequest {
        let placement: PlacementConfig
        let continuation: CheckedContinuation<BidResponse?, Never>
    }

    // MARK: - Init

    init(
        httpClient: HTTPClient,
        configManager: ConfigManager,
        privacyStore: PrivacyStore,
        s2sConfig: S2SConfig
    ) {
        self.httpClient = httpClient
        self.configManager = configManager
        self.privacyStore = privacyStore
        self.s2sConfig = s2sConfig
    }

    // MARK: - Public API

    /**
     * Fetch demand for a single placement
     *
     * The placement is queued and batched with other placements requested within
     * a 100ms window. Returns GAM custom targeting KVPs on success, nil on failure.
     *
     * - Parameter placement: The placement to fetch demand for
     * - Returns: Targeting KVPs to apply to GAM request, or nil
     */
    func fetchDemand(placement: PlacementConfig) async -> BidResponse? {
        guard s2sConfig.enabled else {
            BCLogger.debug("BidRequestClient: S2S is disabled, skipping bid request")
            return nil
        }

        guard placement.enabled else {
            BCLogger.debug("BidRequestClient: Placement disabled, skipping bid request: \(placement.placementId)")
            return nil
        }

        return await withCheckedContinuation { continuation in
            lock.lock()
            pendingRequests.append(PendingRequest(
                placement: placement,
                continuation: continuation
            ))

            if batchTimer == nil {
                let delay = TimeInterval(Self.batchDelayMs) / 1000.0
                let timer = Timer(timeInterval: delay, repeats: false) { [weak self] _ in
                    self?.flushBatch()
                }
                RunLoop.main.add(timer, forMode: .common)
                batchTimer = timer
            }
            lock.unlock()
        }
    }

    // MARK: - Batching

    private func flushBatch() {
        lock.lock()
        let batch = pendingRequests
        pendingRequests = []
        batchTimer?.invalidate()
        batchTimer = nil
        lock.unlock()

        guard !batch.isEmpty else { return }

        let placements = batch.map { $0.placement }
        BCLogger.debug("BidRequestClient: Flushing batch of \(placements.count) placements")

        Task {
            let results = await executeBidRequest(placements: placements)

            // Distribute results to each pending continuation
            for pending in batch {
                let bidResponse = results[pending.placement.placementId]
                pending.continuation.resume(returning: bidResponse)
            }
        }
    }

    // MARK: - Request Building & Execution

    private func executeBidRequest(placements: [PlacementConfig]) async -> [String: BidResponse] {
        let requestBody = buildRequestJSON(placements: placements)

        guard let jsonData = try? JSONSerialization.data(withJSONObject: requestBody),
              let jsonString = String(data: jsonData, encoding: .utf8) else {
            BCLogger.error("BidRequestClient: Failed to serialize bid request")
            return [:]
        }

        BCLogger.verbose("BidRequestClient: Sending bid request to \(s2sConfig.serverUrl)")

        let result = await httpClient.post(
            url: s2sConfig.serverUrl,
            body: jsonString,
            headers: [:]
        )

        switch result {
        case .success(let responseJson):
            return parseResponse(responseJson, placements: placements)
        case .failure(let error):
            BCLogger.error("BidRequestClient: Bid request failed: \(error)")
            return [:]
        }
    }

    /**
     * Build OpenRTB-style bid request JSON with ext.bidders format
     */
    private func buildRequestJSON(placements: [PlacementConfig]) -> [String: Any] {
        var request: [String: Any] = [:]

        // Request ID
        request["id"] = UUID().uuidString

        // Impressions
        request["imp"] = placements.map { buildImp($0) }

        // App object
        let deviceContext = DeviceContext.shared
        request["app"] = [
            "bundle": deviceContext.appBundleId,
            "ver": deviceContext.appVersion,
            "publisher": ["id": BigCrunchAds.propertyId]
        ]

        // Device object
        request["device"] = buildDevice(deviceContext)

        // Privacy: regs & user
        let regs = privacyStore.buildRegsDict()
        if !regs.isEmpty {
            request["regs"] = regs
        }

        let user = privacyStore.buildUserDict()
        if !user.isEmpty {
            request["user"] = user
        }

        // ext.bidders — the new format
        let biddersExt = buildBiddersExt(placements: placements)
        if !biddersExt.isEmpty {
            request["ext"] = ["bidders": biddersExt]
        }

        return request
    }

    private func buildImp(_ placement: PlacementConfig) -> [String: Any] {
        var imp: [String: Any] = [
            "id": placement.id
        ]

        switch placement.format {
        case "banner":
            var banner: [String: Any] = [:]
            if let sizes = placement.sizes {
                banner["format"] = sizes.map { size -> [String: Any] in
                    if size.isAdaptive {
                        // Use screen width for adaptive
                        let screenWidth = DeviceContext.shared.screenWidth
                        return ["w": screenWidth, "h": 0]
                    }
                    return ["w": size.width, "h": size.height]
                }
            }
            imp["banner"] = banner

        case "interstitial":
            imp["instl"] = 1
            imp["banner"] = [
                "format": [
                    ["w": DeviceContext.shared.screenWidth,
                     "h": DeviceContext.shared.screenHeight]
                ]
            ]

        case "rewarded":
            imp["instl"] = 1
            imp["video"] = [
                "mimes": ["video/mp4"],
                "protocols": [2, 5], // VAST 2.0, VAST 2.0 Wrapper
                "w": DeviceContext.shared.screenWidth,
                "h": DeviceContext.shared.screenHeight
            ]
            imp["ext"] = ["rewarded": 1]

        default:
            break
        }

        return imp
    }

    private func buildDevice(_ ctx: DeviceContext) -> [String: Any] {
        var device: [String: Any] = [
            "os": ctx.osName,
            "osv": ctx.osVersion,
            "make": "Apple",
            "model": ctx.deviceModel,
            "w": ctx.screenWidth,
            "h": ctx.screenHeight,
            "pxratio": ctx.screenScale,
            "language": ctx.languageCode
        ]

        // User-Agent — use a reasonable default
        device["ua"] = ctx.getBrowserField()

        return device
    }

    /**
     * Build the ext.bidders section
     *
     * For each bidder in AppConfig.bidders, check which of the requested placements
     * have entries in that bidder's placements map. Only include relevant bidders/imps.
     */
    private func buildBiddersExt(placements: [PlacementConfig]) -> [String: Any] {
        guard let bidders = configManager.getCachedConfig()?.bidders else {
            return [:]
        }

        let placementIdToUuid = Dictionary(uniqueKeysWithValues: placements.map { ($0.placementId, $0.id) })
        let placementIds = Set(placementIdToUuid.keys)
        var biddersExt: [String: Any] = [:]

        for (bidderName, bidderEntry) in bidders {
            var bidderObj: [String: Any] = [:]

            // Shared params
            if let params = bidderEntry.params {
                bidderObj["params"] = params.mapValues { $0.value }
            }

            // Per-imp params (only for placements in this batch)
            var impEntries: [[String: Any]] = []
            if let bidderPlacements = bidderEntry.placements {
                for placementId in placementIds {
                    if let impParams = bidderPlacements[placementId] {
                        impEntries.append([
                            "impid": placementIdToUuid[placementId] ?? placementId,
                            "params": impParams.mapValues { $0.value }
                        ])
                    }
                }
            }

            // Only include bidder if it has relevant impressions
            if !impEntries.isEmpty {
                bidderObj["imp"] = impEntries
                biddersExt[bidderName] = bidderObj
            }
        }

        return biddersExt
    }

    // MARK: - Response Parsing

    /**
     * Parse S2S response and extract per-placement targeting
     *
     * Expected format:
     * ```json
     * {
     *   "seatbid": [{
     *     "bid": [{
     *       "impid": "leaderboard",
     *       "price": 2.50,
     *       "ext": { "targeting": { "hb_pb": "2.50", ... } }
     *     }]
     *   }]
     * }
     * ```
     *
     * Groups bids by impid, picks winner (highest price), copies ext.targeting.
     */
    private func parseResponse(_ jsonString: String, placements: [PlacementConfig]) -> [String: BidResponse] {
        guard let data = jsonString.data(using: .utf8),
              let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
              let seatbids = json["seatbid"] as? [[String: Any]] else {
            BCLogger.error("BidRequestClient: Failed to parse bid response")
            return [:]
        }

        let auctionId = json["id"] as? String

        // Build floor price lookup (keyed by UUID) and UUID-to-placementId mapping
        let floorPrices = Dictionary(uniqueKeysWithValues: placements.map { ($0.id, $0.floorPrice) })
        let uuidToPlacementId = Dictionary(uniqueKeysWithValues: placements.map { ($0.id, $0.placementId) })

        // Collect all bids grouped by impid
        struct BidInfo {
            let price: Double
            let targeting: [String: String]
            let seat: String
            let creativeId: String?
        }

        var bidsByImpId: [String: [BidInfo]] = [:]

        for seatbid in seatbids {
            let seat = seatbid["seat"] as? String ?? ""
            guard let bids = seatbid["bid"] as? [[String: Any]] else { continue }
            for bid in bids {
                guard let impid = bid["impid"] as? String,
                      let price = bid["price"] as? Double,
                      let ext = bid["ext"] as? [String: Any],
                      let targeting = ext["targeting"] as? [String: String] else {
                    continue
                }
                let crid = bid["crid"] as? String
                bidsByImpId[impid, default: []].append(BidInfo(
                    price: price, targeting: targeting, seat: seat, creativeId: crid
                ))
            }
        }

        // Pick winner (highest price) for each impid and compute auction metadata
        var results: [String: BidResponse] = [:]
        for (impid, bids) in bidsByImpId {
            let sortedBids = bids.sorted { $0.price > $1.price }
            guard let winner = sortedBids.first else { continue }

            let secondHighestBid = sortedBids.count > 1 ? sortedBids[1].price : nil
            let floorPrice = floorPrices[impid] ?? nil

            let auctionData = AuctionData(
                auctionId: auctionId,
                bidder: winner.seat,
                bidPriceCpm: winner.price,
                creativeId: winner.creativeId,
                gamPriceBucket: winner.targeting["hb_pb"],
                floorPrice: floorPrice,
                secondHighestBid: secondHighestBid,
                demandChannel: "S2S"
            )

            // Re-key from UUID to placementId for internal use
            let placementId = uuidToPlacementId[impid] ?? impid
            results[placementId] = BidResponse(targeting: winner.targeting, auctionData: auctionData)
            BCLogger.debug("BidRequestClient: Winner for \(placementId): $\(winner.price)")
        }

        BCLogger.info("BidRequestClient: Parsed \(results.count) winning bids")
        return results
    }
}
