import Foundation

/**
 * Configuration for a single ad placement
 *
 * Contains all information needed to request and display an ad at a specific
 * location in the app. Bidder configuration is no longer stored here — it lives
 * in the top-level `AppConfig.bidders` dictionary.
 */
public struct PlacementConfig: Codable {
    public let id: String
    public let placementId: String
    public let format: String // "banner", "interstitial", "rewarded"
    public let gamAdUnit: String
    public let sizes: [AdSize]?
    public let refresh: RefreshConfig?
    public let floorPrice: Double?
    public let enabled: Bool

    enum CodingKeys: String, CodingKey {
        case id, placementId, format, gamAdUnit, sizes, refresh, floorPrice, enabled
    }

    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        id = try container.decode(String.self, forKey: .id)
        placementId = try container.decode(String.self, forKey: .placementId)
        format = try container.decode(String.self, forKey: .format)
        gamAdUnit = try container.decode(String.self, forKey: .gamAdUnit)
        sizes = try container.decodeIfPresent([AdSize].self, forKey: .sizes)
        refresh = try container.decodeIfPresent(RefreshConfig.self, forKey: .refresh)
        floorPrice = try container.decodeIfPresent(Double.self, forKey: .floorPrice)
        enabled = try container.decodeIfPresent(Bool.self, forKey: .enabled) ?? true
    }

    public init(
        id: String,
        placementId: String,
        format: String,
        gamAdUnit: String,
        sizes: [AdSize]?,
        refresh: RefreshConfig? = nil,
        floorPrice: Double? = nil,
        enabled: Bool = true
    ) {
        self.id = id
        self.placementId = placementId
        self.format = format
        self.gamAdUnit = gamAdUnit
        self.sizes = sizes
        self.refresh = refresh
        self.floorPrice = floorPrice
        self.enabled = enabled
    }
}

/**
 * Type-erased Codable wrapper for arbitrary JSON values
 *
 * Supports String, Int, Double, Bool, nested objects, and arrays.
 */
public struct AnyCodable: Codable, Equatable {
    public let value: Any

    public init(_ value: Any) {
        self.value = value
    }

    public init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        if let intVal = try? container.decode(Int.self) {
            value = intVal
        } else if let doubleVal = try? container.decode(Double.self) {
            value = doubleVal
        } else if let boolVal = try? container.decode(Bool.self) {
            value = boolVal
        } else if let stringVal = try? container.decode(String.self) {
            value = stringVal
        } else if let arrayVal = try? container.decode([AnyCodable].self) {
            value = arrayVal.map { $0.value }
        } else if let dictVal = try? container.decode([String: AnyCodable].self) {
            value = dictVal.mapValues { $0.value }
        } else {
            throw DecodingError.dataCorruptedError(in: container, debugDescription: "Unsupported JSON value")
        }
    }

    public func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        switch value {
        case let intVal as Int:
            try container.encode(intVal)
        case let doubleVal as Double:
            try container.encode(doubleVal)
        case let boolVal as Bool:
            try container.encode(boolVal)
        case let stringVal as String:
            try container.encode(stringVal)
        case let arrayVal as [Any]:
            try container.encode(arrayVal.map { AnyCodable($0) })
        case let dictVal as [String: Any]:
            try container.encode(dictVal.mapValues { AnyCodable($0) })
        default:
            throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: encoder.codingPath, debugDescription: "Unsupported value type"))
        }
    }

    public static func == (lhs: AnyCodable, rhs: AnyCodable) -> Bool {
        switch (lhs.value, rhs.value) {
        case let (l as Int, r as Int): return l == r
        case let (l as Double, r as Double): return l == r
        case let (l as Bool, r as Bool): return l == r
        case let (l as String, r as String): return l == r
        default: return false
        }
    }
}

/**
 * Ad size dimensions
 *
 * When `type` is "adaptive" or "smart", the SDK will use Google's adaptive
 * banner API to calculate the optimal height for the given `width`.
 * A `width` of 0 means "use screen width".
 */
/// Typealias for disambiguation when module name collides with BigCrunchAds class name
public typealias BCAdSize = AdSize

public struct AdSize: Codable, Equatable {
    public let width: Int
    public let height: Int
    public let type: String?

    public var isAdaptive: Bool {
        type == "adaptive" || type == "smart"
    }

    public init(width: Int, height: Int, type: String? = nil) {
        self.width = width
        self.height = height
        self.type = type
    }

    public static func adaptive(width: Int = 0) -> AdSize {
        AdSize(width: width, height: 0, type: "adaptive")
    }
}

/**
 * Refresh configuration for banner ads
 */
public struct RefreshConfig: Codable {
    public let enabled: Bool
    public let intervalMs: Int
    public let maxRefreshes: Int

    public init(enabled: Bool, intervalMs: Int, maxRefreshes: Int) {
        self.enabled = enabled
        self.intervalMs = intervalMs
        self.maxRefreshes = maxRefreshes
    }
}
