import Foundation

/// The common MCMS response envelope.
///
/// Success: `{ "status": "success", "data": { ... } }`
/// Failure: `{ "status": "fail",    "details": { ... } }`
///
/// `data` is generic; `details` is free-form so we keep it as `JSONValue`.
struct APIResponse<T: Decodable>: Decodable {
    let status: String
    let data: T?
    let details: JSONValue?

    var isSuccess: Bool { status.lowercased() == "success" }
}

/// Write requests wrap their document under a `data` field:
/// `{ "data": { <document> } }`
struct DataEnvelope<B: Encodable>: Encodable {
    let data: B
}

// MARK: - JSONValue

/// A fully dynamic, `Codable` JSON value. Used to (a) carry the free-form
/// `details` payload on failures and (b) hold MongoDB documents whose full
/// schema we haven't typed yet, so screens can display/edit raw config without
/// every field being modeled up front. Replace with concrete structs as you
/// pin down shapes from `openapi.json`.
indirect enum JSONValue: Codable, Equatable {
    case null
    case bool(Bool)
    case int(Int)
    case double(Double)
    case string(String)
    case array([JSONValue])
    case object([String: JSONValue])

    init(from decoder: Decoder) throws {
        let c = try decoder.singleValueContainer()
        if c.decodeNil() {
            self = .null
        } else if let b = try? c.decode(Bool.self) {
            self = .bool(b)
        } else if let i = try? c.decode(Int.self) {
            self = .int(i)
        } else if let d = try? c.decode(Double.self) {
            self = .double(d)
        } else if let s = try? c.decode(String.self) {
            self = .string(s)
        } else if let a = try? c.decode([JSONValue].self) {
            self = .array(a)
        } else if let o = try? c.decode([String: JSONValue].self) {
            self = .object(o)
        } else {
            throw DecodingError.dataCorruptedError(
                in: c, debugDescription: "Unsupported JSON value")
        }
    }

    func encode(to encoder: Encoder) throws {
        var c = encoder.singleValueContainer()
        switch self {
        case .null: try c.encodeNil()
        case .bool(let b): try c.encode(b)
        case .int(let i): try c.encode(i)
        case .double(let d): try c.encode(d)
        case .string(let s): try c.encode(s)
        case .array(let a): try c.encode(a)
        case .object(let o): try c.encode(o)
        }
    }

    // Convenience accessors
    subscript(_ key: String) -> JSONValue? {
        if case .object(let o) = self { return o[key] }
        return nil
    }
    var stringValue: String? { if case .string(let s) = self { return s }; return nil }
    var intValue: Int? {
        switch self {
        case .int(let i): return i
        // `Int(_:)` traps on NaN/±inf/out-of-range; JSONDecoder can yield those
        // from extreme literals (e.g. `1e400` → .infinity). Guard so a malformed
        // server number becomes nil instead of a crash.
        case .double(let d): return (d.isFinite && d >= Double(Int.min) && d < Double(Int.max)) ? Int(d) : nil
        default: return nil
        }
    }
    var doubleValue: Double? {
        switch self { case .double(let d): return d; case .int(let i): return Double(i); default: return nil }
    }
    var boolValue: Bool? { if case .bool(let b) = self { return b }; return nil }
    var arrayValue: [JSONValue]? { if case .array(let a) = self { return a }; return nil }
    var objectValue: [String: JSONValue]? { if case .object(let o) = self { return o }; return nil }

    /// A short, human-readable rendering for error surfaces.
    var compactDescription: String {
        switch self {
        case .null: return "null"
        case .bool(let b): return "\(b)"
        case .int(let i): return "\(i)"
        case .double(let d): return "\(d)"
        case .string(let s): return s
        case .array(let a): return a.map(\.compactDescription).joined(separator: ", ")
        case .object(let o):
            return o.map { "\($0): \($1.compactDescription)" }.joined(separator: "; ")
        }
    }
}

/// Types that can represent an "empty" value, returned by the client when the
/// server responds 204 / no body to a request whose return type is non-optional.
protocol EmptyRepresentable {
    static var emptyValue: Self { get }
}

extension JSONValue: EmptyRepresentable {
    static var emptyValue: JSONValue { .null }
}
