import Foundation
import UIKit
import GoogleMobileAds

/**
 * Callback protocol for banner ad events
 */
internal protocol BannerAdDelegate: AnyObject {
    func bannerAdDidLoad(_ bannerView: GoogleMobileAds.BannerView)
    func bannerAd(_ bannerView: GoogleMobileAds.BannerView, didFailToLoadWithError error: Error)
    func bannerAdDidRecordClick(_ bannerView: GoogleMobileAds.BannerView)
    func bannerAdDidRecordImpression(_ bannerView: GoogleMobileAds.BannerView)
}

/**
 * Callback protocol for interstitial ad events
 */
internal protocol InterstitialAdDelegate: AnyObject {
    func interstitialAdDidLoad(_ interstitialAd: GoogleMobileAds.InterstitialAd)
    func interstitialAd(didFailToLoadWithError error: Error)
    func interstitialAdDidPresent(_ interstitialAd: GoogleMobileAds.InterstitialAd)
    func interstitialAdDidDismiss(_ interstitialAd: GoogleMobileAds.InterstitialAd)
    func interstitialAdDidRecordClick(_ interstitialAd: GoogleMobileAds.InterstitialAd)
    func interstitialAdDidRecordImpression(_ interstitialAd: GoogleMobileAds.InterstitialAd)
}

/**
 * Adapter for Google Mobile Ads SDK (Ad Manager)
 *
 * Wraps GMA SDK calls and translates callbacks to AnalyticsClient events.
 * Handles banner, interstitial, and rewarded ad formats.
 */
internal class GoogleAdsAdapter: NSObject {

    private static let TAG = "GoogleAdsAdapter"

    // Google's official iOS test ad unit IDs
    // These are guaranteed to return test ads and will always fill
    // See: https://developers.google.com/admob/ios/test-ads
    private static let TEST_BANNER_AD_UNIT = "ca-app-pub-3940256099942544/2934735716"
    private static let TEST_INTERSTITIAL_AD_UNIT = "ca-app-pub-3940256099942544/4411468910"
    private static let TEST_REWARDED_AD_UNIT = "ca-app-pub-3940256099942544/1712485313"

    private let analyticsClient: AnalyticsClient

    init(analyticsClient: AnalyticsClient) {
        self.analyticsClient = analyticsClient
        super.init()
    }

    // MARK: - Ad Unit Resolution

    /**
     * Resolve the ad unit ID to use - test ID if useTestAds is true, otherwise production
     *
     * - Parameters:
     *   - placementConfig: The placement configuration
     *   - gamNetworkCode: The GAM network code from app config
     *   - useTestAds: Whether to use test ad units
     * - Returns: The resolved ad unit ID
     */
    private func resolveAdUnitId(
        placementConfig: PlacementConfig,
        gamNetworkCode: String,
        useTestAds: Bool
    ) -> String {
        if useTestAds {
            let testAdUnit: String
            switch placementConfig.format {
            case "banner":
                testAdUnit = GoogleAdsAdapter.TEST_BANNER_AD_UNIT
            case "interstitial":
                testAdUnit = GoogleAdsAdapter.TEST_INTERSTITIAL_AD_UNIT
            case "rewarded":
                testAdUnit = GoogleAdsAdapter.TEST_REWARDED_AD_UNIT
            default:
                testAdUnit = GoogleAdsAdapter.TEST_BANNER_AD_UNIT // fallback
            }
            BCLogger.debug("\(GoogleAdsAdapter.TAG): Using TEST ad unit for \(placementConfig.format): \(testAdUnit)")
            return testAdUnit
        } else {
            // Production ad unit path
            // - For GAM: /{networkCode}/{adUnit}
            // - For AdMob (empty network code): just the ad unit ID directly
            // gamAdUnit already contains the full path e.g. /networkCode/adUnit
            BCLogger.debug("\(GoogleAdsAdapter.TAG): Using PRODUCTION ad unit: \(placementConfig.gamAdUnit)")
            return placementConfig.gamAdUnit
        }
    }

    // MARK: - Size Resolution

    /// Resolve a BigCrunch AdSize to a Google AdSize.
    /// For adaptive sizes, calculates the optimal ad size based on screen width.
    /// A 0x0 size is always treated as adaptive since it is never valid as a fixed size.
    private func resolveGoogleAdSize(_ bcAdSize: AdSize) -> GoogleMobileAds.AdSize {
        if bcAdSize.isAdaptive || (bcAdSize.width == 0 && bcAdSize.height == 0) {
            let width: CGFloat = bcAdSize.width > 0
                ? CGFloat(bcAdSize.width)
                : UIScreen.main.bounds.width
            BCLogger.debug("\(GoogleAdsAdapter.TAG): Resolving adaptive banner with width: \(width)pt (isAdaptive=\(bcAdSize.isAdaptive), original=\(bcAdSize.width)x\(bcAdSize.height))")
            return GoogleMobileAds.currentOrientationAnchoredAdaptiveBanner(width: width)
        }
        return GoogleMobileAds.adSizeFor(cgSize: CGSize(
            width: CGFloat(bcAdSize.width),
            height: CGFloat(bcAdSize.height)
        ))
    }

    // MARK: - Banner Ads

    /**
     * Create and load a banner ad
     *
     * - Parameters:
     *   - placementConfig: Configuration for the placement
     *   - gamNetworkCode: The GAM network code from app config
     *   - request: Pre-configured GoogleMobileAds.Request (with S2S targeting)
     *   - rootViewController: View controller for presenting ad click actions
     *   - delegate: Delegate for ad events
     *   - useTestAds: Whether to use test ad units (default: false)
     *   - adSizeOverride: Optional size override (takes precedence over backend config)
     * - Returns: The configured GoogleMobileAds.BannerView
     */
    @MainActor
    func loadBannerAd(
        placementConfig: PlacementConfig,
        gamNetworkCode: String,
        request: GoogleMobileAds.Request,
        rootViewController: UIViewController,
        delegate: BannerAdDelegate,
        useTestAds: Bool = false,
        adSizeOverride: AdSize? = nil,
        refreshCount: Int = 0,
        loadId: String? = nil
    ) -> GoogleMobileAds.BannerView {
        BCLogger.debug("\(GoogleAdsAdapter.TAG): Loading banner ad for: \(placementConfig.placementId)")

        // Resolve ad unit ID (test or production)
        let adUnitID = resolveAdUnitId(
            placementConfig: placementConfig,
            gamNetworkCode: gamNetworkCode,
            useTestAds: useTestAds
        )

        let bannerView = GoogleMobileAds.BannerView()
        bannerView.adUnitID = adUnitID
        bannerView.rootViewController = rootViewController

        // Set ad size: use override first, then placement config, then default
        if let overrideSize = adSizeOverride {
            BCLogger.debug("\(GoogleAdsAdapter.TAG): Using size override: \(overrideSize.width)x\(overrideSize.height) (adaptive=\(overrideSize.isAdaptive))")
            bannerView.adSize = resolveGoogleAdSize(overrideSize)
        } else if let size = placementConfig.sizes?.first {
            bannerView.adSize = resolveGoogleAdSize(size)
        } else {
            // Default to standard banner size
            bannerView.adSize = GoogleMobileAds.AdSizeBanner
        }

        // Set up delegate wrapper to handle callbacks
        let delegateWrapper = BannerDelegateWrapper(
            placementConfig: placementConfig,
            analyticsClient: analyticsClient,
            delegate: delegate,
            refreshCount: refreshCount,
            loadId: loadId
        )
        bannerView.delegate = delegateWrapper

        // Store delegate wrapper to prevent deallocation
        objc_setAssociatedObject(
            bannerView,
            &AssociatedKeys.delegateWrapper,
            delegateWrapper,
            .OBJC_ASSOCIATION_RETAIN_NONATOMIC
        )

        // Set up paid event handler for ILRD (Impression-Level Revenue Data)
        bannerView.paidEventHandler = { [weak self, placementConfig] adValue in
            BCLogger.debug("\(GoogleAdsAdapter.TAG): Banner paid event: \(adValue.value) \(adValue.currencyCode) precision=\(adValue.precision.rawValue)")
            // adValue.value is per-impression revenue in currency units; multiply by 1000 to get CPM
            let revenueCpm = Double(truncating: adValue.value) * 1000.0
            // .precise/.estimated → Google's auction provided the value (AdX outbid the S2S line item).
            // .publisherProvided → our hb_pb targeting was used (S2S line item won).
            // .unknown → don't override; trust the pre-set auction data.
            let googleAuctionWon = adValue.precision == .precise || adValue.precision == .estimated
            self?.analyticsClient.handleIlrdRevenue(
                placementId: placementConfig.placementId,
                revenueCpm: revenueCpm,
                googleAuctionWon: googleAuctionWon
            )
        }

        // Load the ad
        bannerView.load(request)

        return bannerView
    }

    /**
     * Destroy a banner ad view
     *
     * - Parameter bannerView: The GoogleMobileAds.BannerView to destroy
     */
    func destroyBannerAd(_ bannerView: GoogleMobileAds.BannerView) {
        BCLogger.debug("\(GoogleAdsAdapter.TAG): Destroying banner ad")
        bannerView.delegate = nil
        bannerView.paidEventHandler = nil
        bannerView.removeFromSuperview()

        // Clear associated delegate wrapper
        objc_setAssociatedObject(
            bannerView,
            &AssociatedKeys.delegateWrapper,
            nil,
            .OBJC_ASSOCIATION_RETAIN_NONATOMIC
        )
    }

    // MARK: - Interstitial Ads

    /**
     * Load an interstitial ad
     *
     * - Parameters:
     *   - placementConfig: Configuration for the placement
     *   - gamNetworkCode: The GAM network code from app config
     *   - request: Pre-configured GoogleMobileAds.Request (with S2S targeting)
     *   - useTestAds: Whether to use test ad units (default: false)
     * - Returns: Result containing loaded interstitial or error
     */
    func loadInterstitialAd(
        placementConfig: PlacementConfig,
        gamNetworkCode: String,
        request: GoogleMobileAds.Request,
        useTestAds: Bool = false
    ) async -> Result<GoogleMobileAds.InterstitialAd, Error> {
        BCLogger.debug("\(GoogleAdsAdapter.TAG): Loading interstitial ad for: \(placementConfig.placementId)")

        // Resolve ad unit ID (test or production)
        let adUnitID = resolveAdUnitId(
            placementConfig: placementConfig,
            gamNetworkCode: gamNetworkCode,
            useTestAds: useTestAds
        )

        return await withCheckedContinuation { continuation in
            GoogleMobileAds.InterstitialAd.load(
                with: adUnitID,
                request: request
            ) { [weak self] interstitialAd, error in
                if let error = error {
                    BCLogger.warning("\(GoogleAdsAdapter.TAG): Interstitial ad failed to load: \(error.localizedDescription)")
                    continuation.resume(returning: .failure(error))
                    return
                }

                guard let interstitialAd = interstitialAd else {
                    BCLogger.warning("\(GoogleAdsAdapter.TAG): Interstitial ad loaded but was nil")
                    continuation.resume(returning: .failure(NSError(
                        domain: "BigCrunchAds",
                        code: -1,
                        userInfo: [NSLocalizedDescriptionKey: "Interstitial ad was nil"]
                    )))
                    return
                }

                BCLogger.debug("\(GoogleAdsAdapter.TAG): Interstitial ad loaded: \(placementConfig.placementId)")

                // Set up paid event handler for ILRD
                interstitialAd.paidEventHandler = { [weak self, placementConfig] adValue in
                    BCLogger.debug("\(GoogleAdsAdapter.TAG): Interstitial paid event: \(adValue.value) \(adValue.currencyCode) precision=\(adValue.precision.rawValue)")
                    // adValue.value is per-impression revenue in currency units; multiply by 1000 to get CPM
                    let revenueCpm = Double(truncating: adValue.value) * 1000.0
                    let googleAuctionWon = adValue.precision == .precise || adValue.precision == .estimated
                    self?.analyticsClient.handleIlrdRevenue(
                        placementId: placementConfig.placementId,
                        revenueCpm: revenueCpm,
                        googleAuctionWon: googleAuctionWon
                    )
                }

                continuation.resume(returning: .success(interstitialAd))
            }
        }
    }

    /**
     * Show an interstitial ad
     *
     * - Parameters:
     *   - interstitialAd: The loaded interstitial ad
     *   - viewController: View controller to present from
     *   - placementConfig: Configuration for the placement
     *   - delegate: Delegate for ad events
     */
    func showInterstitialAd(
        _ interstitialAd: GoogleMobileAds.InterstitialAd,
        from viewController: UIViewController,
        placementConfig: PlacementConfig,
        delegate: InterstitialAdDelegate,
        loadId: String? = nil
    ) {
        BCLogger.debug("\(GoogleAdsAdapter.TAG): Showing interstitial ad: \(placementConfig.placementId)")

        // Set up full screen content delegate
        let delegateWrapper = InterstitialDelegateWrapper(
            placementConfig: placementConfig,
            analyticsClient: analyticsClient,
            delegate: delegate,
            loadId: loadId
        )
        interstitialAd.fullScreenContentDelegate = delegateWrapper

        // Store delegate wrapper to prevent deallocation
        objc_setAssociatedObject(
            interstitialAd,
            &AssociatedKeys.delegateWrapper,
            delegateWrapper,
            .OBJC_ASSOCIATION_RETAIN_NONATOMIC
        )

        // Present the ad
        interstitialAd.present(from: viewController)
    }

    // MARK: - Rewarded Ads

    /**
     * Load a rewarded ad
     *
     * - Parameters:
     *   - placementConfig: Configuration for the placement
     *   - gamNetworkCode: The GAM network code from app config
     *   - request: Pre-configured GoogleMobileAds.Request (with S2S targeting)
     *   - useTestAds: Whether to use test ad units (default: false)
     * - Returns: Result containing loaded rewarded ad or error
     */
    func loadRewardedAd(
        placementConfig: PlacementConfig,
        gamNetworkCode: String,
        request: GoogleMobileAds.Request,
        useTestAds: Bool = false
    ) async -> Result<GoogleMobileAds.RewardedAd, Error> {
        BCLogger.debug("\(GoogleAdsAdapter.TAG): Loading rewarded ad for: \(placementConfig.placementId)")

        // Resolve ad unit ID (test or production)
        let adUnitID = resolveAdUnitId(
            placementConfig: placementConfig,
            gamNetworkCode: gamNetworkCode,
            useTestAds: useTestAds
        )

        return await withCheckedContinuation { continuation in
            GoogleMobileAds.RewardedAd.load(
                with: adUnitID,
                request: request
            ) { [weak self] rewardedAd, error in
                if let error = error {
                    BCLogger.warning("\(GoogleAdsAdapter.TAG): Rewarded ad failed to load: \(error.localizedDescription)")
                    continuation.resume(returning: .failure(error))
                    return
                }

                guard let rewardedAd = rewardedAd else {
                    BCLogger.warning("\(GoogleAdsAdapter.TAG): Rewarded ad loaded but was nil")
                    continuation.resume(returning: .failure(NSError(
                        domain: "BigCrunchAds",
                        code: -1,
                        userInfo: [NSLocalizedDescriptionKey: "Rewarded ad was nil"]
                    )))
                    return
                }

                BCLogger.debug("\(GoogleAdsAdapter.TAG): Rewarded ad loaded: \(placementConfig.placementId)")

                // Set up paid event handler for ILRD
                rewardedAd.paidEventHandler = { [weak self, placementConfig] adValue in
                    BCLogger.debug("\(GoogleAdsAdapter.TAG): Rewarded paid event: \(adValue.value) \(adValue.currencyCode) precision=\(adValue.precision.rawValue)")
                    // adValue.value is per-impression revenue in currency units; multiply by 1000 to get CPM
                    let revenueCpm = Double(truncating: adValue.value) * 1000.0
                    let googleAuctionWon = adValue.precision == .precise || adValue.precision == .estimated
                    self?.analyticsClient.handleIlrdRevenue(
                        placementId: placementConfig.placementId,
                        revenueCpm: revenueCpm,
                        googleAuctionWon: googleAuctionWon
                    )
                }

                continuation.resume(returning: .success(rewardedAd))
            }
        }
    }
}

// MARK: - Associated Object Keys

private struct AssociatedKeys {
    static var delegateWrapper = "delegateWrapper"
}

// MARK: - Banner Delegate Wrapper

/**
 * Internal delegate wrapper that handles GMA banner callbacks and
 * forwards them to both AnalyticsClient and the public delegate.
 */
private class BannerDelegateWrapper: NSObject, GoogleMobileAds.BannerViewDelegate {

    private let placementConfig: PlacementConfig
    private let analyticsClient: AnalyticsClient
    private weak var delegate: BannerAdDelegate?
    private let refreshCount: Int
    private let loadId: String?

    init(
        placementConfig: PlacementConfig,
        analyticsClient: AnalyticsClient,
        delegate: BannerAdDelegate,
        refreshCount: Int = 0,
        loadId: String? = nil
    ) {
        self.placementConfig = placementConfig
        self.analyticsClient = analyticsClient
        self.delegate = delegate
        self.refreshCount = refreshCount
        self.loadId = loadId
    }

    func bannerViewDidReceiveAd(_ bannerView: GoogleMobileAds.BannerView) {
        BCLogger.debug("GoogleAdsAdapter: Banner ad loaded: \(placementConfig.placementId) (refreshCount: \(refreshCount))")
        delegate?.bannerAdDidLoad(bannerView)
    }

    func bannerView(_ bannerView: GoogleMobileAds.BannerView, didFailToReceiveAdWithError error: Error) {
        BCLogger.warning("GoogleAdsAdapter: Banner ad failed to load: \(error.localizedDescription)")
        if let loadId = loadId {
            analyticsClient.discardAuctionData(loadId: loadId)
        }
        delegate?.bannerAd(bannerView, didFailToLoadWithError: error)
    }

    func bannerViewDidRecordClick(_ bannerView: GoogleMobileAds.BannerView) {
        BCLogger.debug("GoogleAdsAdapter: Banner ad clicked: \(placementConfig.placementId)")
        analyticsClient.trackAdClick(
            placementId: placementConfig.placementId,
            slotId: placementConfig.id,
            format: placementConfig.format
        )
        delegate?.bannerAdDidRecordClick(bannerView)
    }

    func bannerViewDidRecordImpression(_ bannerView: GoogleMobileAds.BannerView) {
        BCLogger.debug("GoogleAdsAdapter: Banner ad impression recorded: \(placementConfig.placementId)")

        // Extract GAM metadata from ResponseInfo at impression time
        let responseInfo = bannerView.responseInfo
        let adMetadata = GoogleAdsAdapter.extractAdMetadata(from: responseInfo)

        // Extract ad size from the loaded banner
        let adSize = bannerView.adSize
        let adSizeString = "\(Int(adSize.size.width))x\(Int(adSize.size.height))"

        // Track on impression-record (not load) so the deferred-impression window
        // co-aligns with the GMA paid event for ILRD-driven AdX revenue.
        analyticsClient.trackAdImpression(
            placementId: placementConfig.placementId,
            slotId: placementConfig.id,
            format: placementConfig.format,
            gamAdUnit: placementConfig.gamAdUnit,
            adSize: adSizeString,
            refreshCount: refreshCount,
            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?.bannerAdDidRecordImpression(bannerView)
    }
}

// MARK: - Interstitial Delegate Wrapper

/**
 * Internal delegate wrapper that handles GMA interstitial callbacks and
 * forwards them to both AnalyticsClient and the public delegate.
 */
private class InterstitialDelegateWrapper: NSObject, GoogleMobileAds.FullScreenContentDelegate {

    private let placementConfig: PlacementConfig
    private let analyticsClient: AnalyticsClient
    private weak var delegate: InterstitialAdDelegate?
    private let loadId: String?

    init(
        placementConfig: PlacementConfig,
        analyticsClient: AnalyticsClient,
        delegate: InterstitialAdDelegate,
        loadId: String? = nil
    ) {
        self.placementConfig = placementConfig
        self.analyticsClient = analyticsClient
        self.delegate = delegate
        self.loadId = loadId
    }

    func adWillPresentFullScreenContent(_ ad: GoogleMobileAds.FullScreenPresentingAd) {
        guard let interstitialAd = ad as? GoogleMobileAds.InterstitialAd else { return }
        BCLogger.debug("GoogleAdsAdapter: Interstitial ad shown: \(placementConfig.placementId)")

        // Extract GAM metadata from ResponseInfo
        let responseInfo = interstitialAd.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?.interstitialAdDidPresent(interstitialAd)
    }

    func adDidDismissFullScreenContent(_ ad: GoogleMobileAds.FullScreenPresentingAd) {
        guard let interstitialAd = ad as? GoogleMobileAds.InterstitialAd else { return }
        BCLogger.debug("GoogleAdsAdapter: Interstitial ad dismissed: \(placementConfig.placementId)")
        delegate?.interstitialAdDidDismiss(interstitialAd)
    }

    func ad(_ ad: GoogleMobileAds.FullScreenPresentingAd, didFailToPresentFullScreenContentWithError error: Error) {
        BCLogger.warning("GoogleAdsAdapter: Interstitial 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)
        }
        delegate?.interstitialAd(didFailToLoadWithError: error)
    }

    func adDidRecordClick(_ ad: GoogleMobileAds.FullScreenPresentingAd) {
        guard let interstitialAd = ad as? GoogleMobileAds.InterstitialAd else { return }
        BCLogger.debug("GoogleAdsAdapter: Interstitial ad clicked: \(placementConfig.placementId)")
        analyticsClient.trackAdClick(
            placementId: placementConfig.placementId,
            slotId: placementConfig.id,
            format: placementConfig.format
        )
        delegate?.interstitialAdDidRecordClick(interstitialAd)
    }

    func adDidRecordImpression(_ ad: GoogleMobileAds.FullScreenPresentingAd) {
        guard let interstitialAd = ad as? GoogleMobileAds.InterstitialAd else { return }
        BCLogger.debug("GoogleAdsAdapter: Interstitial ad impression: \(placementConfig.placementId)")
        delegate?.interstitialAdDidRecordImpression(interstitialAd)
    }
}

// MARK: - Metadata Extraction

extension GoogleAdsAdapter {
    /**
     * Extract GAM metadata from ResponseInfo
     *
     * Google Ad Manager provides limited metadata in ResponseInfo. The main fields available are:
     * - adSourceID: Can contain advertiser ID or network ID
     * - responseIdentifier: Unique response identifier (can be used as creative ID proxy)
     *
     * Note: GAM doesn't directly expose campaignId or lineItemId in mobile SDK ResponseInfo.
     * These fields may be available in server-side logs but not in the client SDK.
     *
     * - Parameter responseInfo: The ResponseInfo from the ad response
     * - Returns: Dictionary of available metadata fields
     */
    static func extractAdMetadata(from responseInfo: GoogleMobileAds.ResponseInfo?) -> [String: Any] {
        guard let responseInfo = responseInfo else {
            return [:]
        }

        var metadata: [String: Any] = [:]

        // Extract adSourceID (often contains advertiser/network ID)
        if let loadedAdapter = responseInfo.loadedAdNetworkResponseInfo {
            if let adSourceID = loadedAdapter.adSourceID {
                metadata["advertiser_id"] = adSourceID
            }
            if let adSourceInstanceID = loadedAdapter.adSourceInstanceID {
                metadata["creative_id"] = adSourceInstanceID
            }
        }

        // Response ID can serve as a creative identifier
        if let responseIdentifier = responseInfo.responseIdentifier, !responseIdentifier.isEmpty {
            // Use responseIdentifier as creative_id if we don't have a better one
            if metadata["creative_id"] == nil {
                metadata["creative_id"] = responseIdentifier
            }
        }

        // Log extracted metadata for debugging
        if !metadata.isEmpty {
            BCLogger.debug("GoogleAdsAdapter: Extracted GAM metadata: \(metadata)")
        }

        return metadata
    }
}
