import UIKit
import GoogleMobileAds

/**
 * BigCrunch Rewarded Ads - Static API for rewarded video ads
 *
 * Rewarded ads are full-screen ads that reward users for watching video content.
 * Users opt-in to watch the ad in exchange for in-app rewards.
 *
 * Usage:
 * ```swift
 * // 1. Preload the ad (do this early, e.g., when entering a screen)
 * BigCrunchRewarded.preload(placementId: "rewarded_video") { result in
 *     switch result {
 *     case .success:
 *         print("Ad ready to show")
 *     case .failure(let error):
 *         print("Failed to load: \(error)")
 *     }
 * }
 *
 * // 2. Check if ready (optional)
 * if BigCrunchRewarded.isReady(placementId: "rewarded_video") {
 *     // Ad is ready to show
 * }
 *
 * // 3. Show when user opts in
 * BigCrunchRewarded.show(
 *     from: self,
 *     placementId: "rewarded_video",
 *     delegate: self
 * )
 * ```
 *
 * Implement BigCrunchRewardedDelegate for events:
 * ```swift
 * extension ViewController: BigCrunchRewardedDelegate {
 *     func rewardedDidShow(placementId: String) { }
 *     func rewardedDidDismiss(placementId: String) { }
 *     func rewardedDidClick(placementId: String) { }
 *     func rewardedDidFail(placementId: String, error: String) { }
 *     func rewardedDidEarnReward(placementId: String, type: String, amount: Int) {
 *         // Grant the reward to the user
 *     }
 * }
 * ```
 */
public final class BigCrunchRewarded {

    private static let TAG = "BigCrunchRewarded"

    // Cache for preloaded rewarded ads, paired with the loadId of the
    // S2S auction data stored for that specific load
    private static var rewardedCache: [String: (ad: RewardedAd, loadId: String)] = [:]
    private static let cacheLock = NSLock()

    // Store delegates to prevent deallocation during show
    private static var activeDelegates: [String: RewardedDelegateWrapper] = [:]
    private static let delegateLock = NSLock()

    // Private initializer to prevent instantiation
    private init() {}

    // MARK: - Legacy Callback Protocol

    /**
     * Legacy callback protocol for rewarded ad events
     */
    public protocol Callback {
        func onUserEarnedReward(type: String, amount: Int)
        func onAdDismissed()
        func onAdFailed(error: String)
    }

    // MARK: - Preload Methods

    /**
     * Preload a rewarded ad for the given placement
     *
     * Call this early (e.g., when entering a screen) to give the ad time to load.
     *
     * - Parameters:
     *   - placementId: The placement ID from BigCrunch dashboard
     *   - completion: Optional completion handler called when preload finishes
     */
    public static func preload(
        placementId: String,
        completion: ((Result<Void, Error>) -> Void)? = nil
    ) {
        BCLogger.debug("\(TAG): Preloading rewarded: \(placementId)")

        BigCrunchAds.requireInitialized()

        guard !placementId.isEmpty else {
            BCLogger.error("\(TAG): Invalid placementId: cannot be empty")
            completion?(.failure(BigCrunchError.invalidPlacement("placementId cannot be empty")))
            return
        }

        // Check if already cached
        cacheLock.lock()
        if rewardedCache[placementId] != nil {
            cacheLock.unlock()
            BCLogger.debug("\(TAG): Rewarded already cached: \(placementId)")
            completion?(.success(()))
            return
        }
        cacheLock.unlock()

        // Get placement config
        let configManager = BigCrunchAds.getConfigManager()
        let analyticsClient = BigCrunchAds.getAnalyticsClient()

        guard let placement = configManager.getPlacement(placementId) else {
            BCLogger.error("\(TAG): Placement not found: \(placementId)")
            analyticsClient.trackUnfilledImpression(placementId: placementId, slotId: placementId, format: "rewarded")
            completion?(.failure(BigCrunchError.invalidPlacement("Placement not found: \(placementId)")))
            return
        }

        guard placement.format == "rewarded" else {
            BCLogger.error("\(TAG): Invalid format for rewarded: \(placement.format)")
            analyticsClient.trackUnfilledImpression(placementId: placementId, slotId: placement.id, format: placement.format, gamAdUnit: placement.gamAdUnit)
            completion?(.failure(BigCrunchError.invalidPlacement("Invalid placement format: \(placement.format)")))
            return
        }

        guard placement.enabled else {
            BCLogger.debug("\(TAG): Placement disabled: \(placementId)")
            analyticsClient.trackUnfilledImpression(placementId: placementId, slotId: placement.id, format: placement.format, gamAdUnit: placement.gamAdUnit)
            completion?(.failure(BigCrunchError.invalidPlacement("Placement is disabled: \(placementId)")))
            return
        }

        // Track ad request
        analyticsClient.trackAdRequest(placementId: placementId, format: placement.format)

        // Load the rewarded ad
        Task {
            await loadRewardedAsync(
                placement: placement,
                configManager: configManager,
                analyticsClient: analyticsClient,
                completion: completion
            )
        }
    }

    /**
     * Preload a rewarded ad (simplified version without completion)
     *
     * - Parameter placementId: The placement ID from BigCrunch dashboard
     */
    public static func preload(placementId: String) {
        preload(placementId: placementId, completion: nil)
    }

    private static func loadRewardedAsync(
        placement: PlacementConfig,
        configManager: ConfigManager,
        analyticsClient: AnalyticsClient,
        completion: ((Result<Void, Error>) -> Void)?
    ) async {
        // Get GAM network code from config
        guard let gamNetworkCode = configManager.getGamNetworkCode() else {
            BCLogger.error("\(TAG): GAM network code not available")
            analyticsClient.trackUnfilledImpression(placementId: placement.placementId, slotId: placement.id, format: placement.format, gamAdUnit: placement.gamAdUnit)
            await MainActor.run {
                completion?(.failure(BigCrunchError.loadFailed("Config not loaded")))
            }
            return
        }

        // Create ad request
        let request = Request()

        // Auction data is keyed by a per-load ID so concurrent loads don't clobber each other
        let loadId = UUID().uuidString

        // Fetch S2S demand
        if let bidRequestClient = BigCrunchAds.getBidRequestClient() {
            let bidResponse = await bidRequestClient.fetchDemand(placement: placement)
            if let targeting = bidResponse?.targeting, !targeting.isEmpty {
                var customTargeting = request.customTargeting ?? [:]
                for (key, value) in targeting {
                    customTargeting[key] = value
                }
                request.customTargeting = customTargeting
                BCLogger.debug("\(TAG): S2S demand fetched for: \(placement.placementId) (\(targeting.count) keys)")
            } else {
                BCLogger.warning("\(TAG): S2S demand fetch returned no targeting, continuing with Google only")
            }

            // Store auction data for analytics
            let auctionData = bidResponse?.auctionData ?? AuctionData(
                auctionId: nil, bidder: "google_ad_exchange", bidPriceCpm: nil, creativeId: nil,
                gamPriceBucket: nil, floorPrice: placement.floorPrice, secondHighestBid: nil,
                demandChannel: "Google Ad Exchange"
            )
            BigCrunchAds.getAnalyticsClient().setAuctionData(loadId: loadId, auctionData: auctionData)
        }

        // Load Google rewarded ad through the adapter (handles ad unit resolution,
        // test-ads substitution, and ILRD paid event wiring)
        let adapter = GoogleAdsAdapter(analyticsClient: analyticsClient)
        let result = await adapter.loadRewardedAd(
            placementConfig: placement,
            gamNetworkCode: gamNetworkCode,
            request: request,
            useTestAds: configManager.shouldUseTestAds()
        )

        switch result {
        case .success(let rewardedAd):
            // Cache the ad
            cacheLock.lock()
            rewardedCache[placement.placementId] = (ad: rewardedAd, loadId: loadId)
            cacheLock.unlock()

            await MainActor.run {
                completion?(.success(()))
            }

        case .failure(let error):
            analyticsClient.discardAuctionData(loadId: loadId)
            analyticsClient.trackUnfilledImpression(placementId: placement.placementId, slotId: placement.id, format: placement.format, gamAdUnit: placement.gamAdUnit)
            await MainActor.run {
                completion?(.failure(BigCrunchError.loadFailed(error.localizedDescription)))
            }
        }
    }

    // MARK: - Show Methods

    /**
     * Show a preloaded rewarded ad
     *
     * The ad must be preloaded first using `preload(placementId:)`. Check `isReady(placementId:)`
     * before calling this to ensure an ad is available.
     *
     * - Parameters:
     *   - viewController: The view controller to present the ad from
     *   - placementId: The placement ID
     *   - delegate: Optional delegate for ad events
     * - Returns: true if a preloaded ad was available and presentation was initiated;
     *   presentation failures are delivered asynchronously via the delegate
     */
    @discardableResult
    public static func show(
        from viewController: UIViewController,
        placementId: String,
        delegate: BigCrunchRewardedDelegate? = nil
    ) -> Bool {
        BCLogger.debug("\(TAG): Showing rewarded: \(placementId)")

        BigCrunchAds.requireInitialized()

        let analyticsClient = BigCrunchAds.getAnalyticsClient()

        guard !placementId.isEmpty else {
            BCLogger.error("\(TAG): Invalid placementId: cannot be empty")
            analyticsClient.trackUnfilledImpression(placementId: placementId, slotId: placementId, format: "rewarded")
            delegate?.rewardedDidFail(placementId: placementId, error: "Invalid placementId")
            return false
        }

        // Get cached rewarded ad
        cacheLock.lock()
        guard let cached = rewardedCache.removeValue(forKey: placementId) else {
            cacheLock.unlock()
            BCLogger.warning("\(TAG): No preloaded rewarded for: \(placementId)")
            analyticsClient.trackUnfilledImpression(placementId: placementId, slotId: placementId, format: "rewarded")
            delegate?.rewardedDidFail(placementId: placementId, error: "No preloaded ad available")
            return false
        }
        cacheLock.unlock()
        let rewardedAd = cached.ad

        // Get placement config for analytics
        let configManager = BigCrunchAds.getConfigManager()
        guard let placement = configManager.getPlacement(placementId) else {
            BCLogger.error("\(TAG): Placement not found: \(placementId)")
            analyticsClient.trackUnfilledImpression(placementId: placementId, slotId: placementId, format: "rewarded")
            delegate?.rewardedDidFail(placementId: placementId, error: "Placement not found")
            return false
        }
        let wrapper = RewardedDelegateWrapper(
            placementId: placementId,
            placementConfig: placement,
            analyticsClient: analyticsClient,
            delegate: delegate,
            loadId: cached.loadId
        ) { pid in
            // Cleanup delegate wrapper after ad is dismissed or fails
            delegateLock.lock()
            activeDelegates.removeValue(forKey: pid)
            delegateLock.unlock()
        }

        delegateLock.lock()
        activeDelegates[placementId] = wrapper
        delegateLock.unlock()

        rewardedAd.fullScreenContentDelegate = wrapper

        // Present the ad
        rewardedAd.present(from: viewController) {
            // User earned reward
            let reward = rewardedAd.adReward
            BCLogger.debug("\(TAG): User earned reward: \(reward.amount) \(reward.type)")
            delegate?.rewardedDidEarnReward(
                placementId: placementId,
                type: reward.type,
                amount: reward.amount.intValue
            )
        }

        return true
    }

    /**
     * Show a preloaded rewarded ad (legacy API)
     *
     * - Parameters:
     *   - viewController: The view controller to present the ad from
     *   - placementId: The placement ID
     *   - callback: Legacy callback for ad events
     */
    public static func show(
        viewController: UIViewController,
        placementId: String,
        callback: Callback? = nil
    ) {
        let delegate: BigCrunchRewardedDelegate? = callback.map { cb in
            LegacyRewardedCallbackAdapter(callback: cb)
        }
        show(from: viewController, placementId: placementId, delegate: delegate)
    }

    // MARK: - Query Methods

    /**
     * Check if a rewarded ad is ready to show
     *
     * - Parameter placementId: The placement ID
     * - Returns: true if an ad is preloaded and ready to show
     */
    public static func isReady(placementId: String) -> Bool {
        cacheLock.lock()
        defer { cacheLock.unlock() }
        return rewardedCache[placementId] != nil
    }

    /**
     * Clear all cached rewarded ads
     *
     * Call this to free up memory when ads are no longer needed.
     */
    public static func clearCache() {
        BCLogger.debug("\(TAG): Clearing rewarded cache")
        cacheLock.lock()
        rewardedCache.removeAll()
        cacheLock.unlock()
    }

    // MARK: - Testing Support

    internal static func resetForTesting() {
        cacheLock.lock()
        rewardedCache.removeAll()
        cacheLock.unlock()

        delegateLock.lock()
        activeDelegates.removeAll()
        delegateLock.unlock()
    }
}

// MARK: - Delegate Protocol

/**
 * Delegate protocol for BigCrunchRewarded events
 */
public protocol BigCrunchRewardedDelegate: AnyObject {
    /**
     * Called when the rewarded ad is displayed on screen
     */
    func rewardedDidShow(placementId: String)

    /**
     * Called when the rewarded ad is dismissed by the user
     */
    func rewardedDidDismiss(placementId: String)

    /**
     * Called when the user clicks on the rewarded ad
     */
    func rewardedDidClick(placementId: String)

    /**
     * Called when the rewarded ad fails to show
     */
    func rewardedDidFail(placementId: String, error: String)

    /**
     * Called when the user earns a reward
     *
     * - Parameters:
     *   - placementId: The placement ID
     *   - type: The reward type (e.g., "coins", "points")
     *   - amount: The reward amount
     */
    func rewardedDidEarnReward(placementId: String, type: String, amount: Int)
}

// MARK: - Default Implementations

public extension BigCrunchRewardedDelegate {
    func rewardedDidShow(placementId: String) {}
    func rewardedDidDismiss(placementId: String) {}
    func rewardedDidClick(placementId: String) {}
    func rewardedDidFail(placementId: String, error: String) {}
    func rewardedDidEarnReward(placementId: String, type: String, amount: Int) {}
}

// MARK: - Private Helper Classes

private class RewardedDelegateWrapper: NSObject, FullScreenContentDelegate {
    let placementId: String
    let placementConfig: PlacementConfig
    let analyticsClient: AnalyticsClient
    weak var delegate: BigCrunchRewardedDelegate?
    let loadId: String?
    let cleanup: (String) -> Void

    init(
        placementId: String,
        placementConfig: PlacementConfig,
        analyticsClient: AnalyticsClient,
        delegate: BigCrunchRewardedDelegate?,
        loadId: String? = nil,
        cleanup: @escaping (String) -> Void
    ) {
        self.placementId = placementId
        self.placementConfig = placementConfig
        self.analyticsClient = analyticsClient
        self.delegate = delegate
        self.loadId = loadId
        self.cleanup = cleanup
    }

    func adWillPresentFullScreenContent(_ ad: FullScreenPresentingAd) {
        BCLogger.debug("BigCrunchRewarded: Rewarded ad shown: \(placementId)")

        // Extract GAM metadata from ResponseInfo
        if let rewardedAd = ad as? RewardedAd {
            let responseInfo = rewardedAd.responseInfo
            let adMetadata = GoogleAdsAdapter.extractAdMetadata(from: responseInfo)

            analyticsClient.trackAdImpression(
                placementId: placementConfig.placementId,
                slotId: placementConfig.id,
                format: placementConfig.format,
                gamAdUnit: placementConfig.gamAdUnit,
                advertiserId: adMetadata["advertiser_id"] as? String,
                campaignId: adMetadata["campaign_id"] as? String,
                lineItemId: adMetadata["line_item_id"] as? String,
                creativeId: adMetadata["creative_id"] as? String,
                loadId: loadId
            )
        }

        delegate?.rewardedDidShow(placementId: placementId)
    }

    func adDidDismissFullScreenContent(_ ad: FullScreenPresentingAd) {
        BCLogger.debug("BigCrunchRewarded: Rewarded ad dismissed: \(placementId)")
        delegate?.rewardedDidDismiss(placementId: placementId)
        cleanup(placementId)
    }

    func ad(_ ad: FullScreenPresentingAd, didFailToPresentFullScreenContentWithError error: Error) {
        BCLogger.warning("BigCrunchRewarded: Rewarded ad failed to show: \(error.localizedDescription)")
        // No impression will fire for this load; drop its stored auction data
        if let loadId = loadId {
            analyticsClient.discardAuctionData(loadId: loadId)
        }
        analyticsClient.trackUnfilledImpression(
            placementId: placementConfig.placementId,
            slotId: placementConfig.id,
            format: placementConfig.format,
            gamAdUnit: placementConfig.gamAdUnit
        )
        delegate?.rewardedDidFail(placementId: placementId, error: error.localizedDescription)
        cleanup(placementId)
    }

    func adDidRecordClick(_ ad: FullScreenPresentingAd) {
        BCLogger.debug("BigCrunchRewarded: Rewarded ad clicked: \(placementId)")
        analyticsClient.trackAdClick(
            placementId: placementConfig.placementId,
            slotId: placementConfig.id,
            format: placementConfig.format
        )
        delegate?.rewardedDidClick(placementId: placementId)
    }

    func adDidRecordImpression(_ ad: FullScreenPresentingAd) {
        BCLogger.debug("BigCrunchRewarded: Rewarded ad impression: \(placementId)")
    }
}

private class LegacyRewardedCallbackAdapter: BigCrunchRewardedDelegate {
    let callback: BigCrunchRewarded.Callback

    init(callback: BigCrunchRewarded.Callback) {
        self.callback = callback
    }

    func rewardedDidShow(placementId: String) {
        // Not in legacy callback
    }

    func rewardedDidDismiss(placementId: String) {
        callback.onAdDismissed()
    }

    func rewardedDidClick(placementId: String) {
        // Not in legacy callback
    }

    func rewardedDidFail(placementId: String, error: String) {
        callback.onAdFailed(error: error)
    }

    func rewardedDidEarnReward(placementId: String, type: String, amount: Int) {
        callback.onUserEarnedReward(type: type, amount: amount)
    }
}
