//
//  NavvoJSON.swift
//  Navvo Maps SDK for iOS
//
//  A Codable, forward-compatible JSON value. Response models keep the full
//  payload as `raw: NavvoJSON` and expose typed accessors for the common
//  fields, so new server fields never break decoding.
//

import Foundation

/// A JSON value: `null`, boolean, number, string, array or object.
public enum NavvoJSON: Equatable, Sendable {
    case null
    case bool(Bool)
    case number(Double)
    case string(String)
    case array([NavvoJSON])
    case object([String: NavvoJSON])
}

// MARK: - Codable

extension NavvoJSON: Codable {
    public init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        if container.decodeNil() {
            self = .null
        } else if let bool = try? container.decode(Bool.self) {
            self = .bool(bool)
        } else if let number = try? container.decode(Double.self) {
            self = .number(number)
        } else if let string = try? container.decode(String.self) {
            self = .string(string)
        } else if let array = try? container.decode([NavvoJSON].self) {
            self = .array(array)
        } else if let object = try? container.decode([String: NavvoJSON].self) {
            self = .object(object)
        } else {
            throw DecodingError.dataCorruptedError(
                in: container, debugDescription: "Unsupported JSON value"
            )
        }
    }

    public func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        switch self {
        case .null: try container.encodeNil()
        case .bool(let bool): try container.encode(bool)
        case .number(let number): try container.encode(number)
        case .string(let string): try container.encode(string)
        case .array(let array): try container.encode(array)
        case .object(let object): try container.encode(object)
        }
    }
}

// MARK: - Accessors

extension NavvoJSON {
    /// Object member access; `nil` when this value is not an object or the
    /// key is absent.
    public subscript(key: String) -> NavvoJSON? {
        guard case .object(let object) = self else { return nil }
        return object[key]
    }

    /// Array element access; `nil` when out of bounds or not an array.
    public subscript(index: Int) -> NavvoJSON? {
        guard case .array(let array) = self, array.indices.contains(index) else { return nil }
        return array[index]
    }

    /// The string payload, when this value is a string.
    public var stringValue: String? {
        if case .string(let string) = self { return string }
        return nil
    }

    /// The boolean payload, when this value is a boolean.
    public var boolValue: Bool? {
        if case .bool(let bool) = self { return bool }
        return nil
    }

    /// The numeric payload. String numbers are parsed too (several Navvo
    /// endpoints send numbers as strings, e.g. geocode `lat`/`lon`).
    public var doubleValue: Double? {
        switch self {
        case .number(let number): return number
        case .string(let string): return Double(string)
        default: return nil
        }
    }

    /// The integer payload (lenient — parses numeric strings).
    public var intValue: Int? {
        switch self {
        case .number(let number): return Int(number)
        case .string(let string):
            if let int = Int(string) { return int }
            if let double = Double(string) { return Int(double) }
            return nil
        default: return nil
        }
    }

    /// The element list, when this value is an array.
    public var arrayValue: [NavvoJSON]? {
        if case .array(let array) = self { return array }
        return nil
    }

    /// The member map, when this value is an object.
    public var objectValue: [String: NavvoJSON]? {
        if case .object(let object) = self { return object }
        return nil
    }

    /// `true` when this value is JSON `null`.
    public var isNull: Bool { self == .null }
}

// MARK: - Literals (ergonomic request-body building)

extension NavvoJSON:
    ExpressibleByNilLiteral,
    ExpressibleByBooleanLiteral,
    ExpressibleByIntegerLiteral,
    ExpressibleByFloatLiteral,
    ExpressibleByStringLiteral,
    ExpressibleByArrayLiteral,
    ExpressibleByDictionaryLiteral
{
    public init(nilLiteral: ()) { self = .null }
    public init(booleanLiteral value: Bool) { self = .bool(value) }
    public init(integerLiteral value: Int) { self = .number(Double(value)) }
    public init(floatLiteral value: Double) { self = .number(value) }
    public init(stringLiteral value: String) { self = .string(value) }
    public init(arrayLiteral elements: NavvoJSON...) { self = .array(elements) }
    public init(dictionaryLiteral elements: (String, NavvoJSON)...) {
        var object: [String: NavvoJSON] = [:]
        for (key, value) in elements { object[key] = value }
        self = .object(object)
    }
}

// MARK: - Data bridging

extension NavvoJSON {
    /// Build from a Foundation JSON object graph (`JSONSerialization` output).
    public init(jsonObject: Any) {
        switch jsonObject {
        case is NSNull:
            self = .null
        case let number as NSNumber:
            // NSNumber-wrapped booleans report objCType "c".
            if String(cString: number.objCType) == "c" {
                self = .bool(number.boolValue)
            } else {
                self = .number(number.doubleValue)
            }
        case let string as String:
            self = .string(string)
        case let array as [Any]:
            self = .array(array.map(NavvoJSON.init(jsonObject:)))
        case let dictionary as [String: Any]:
            self = .object(dictionary.mapValues(NavvoJSON.init(jsonObject:)))
        default:
            self = .null
        }
    }

    /// Parse raw response bytes; `nil` when empty or not valid JSON.
    public static func parse(_ data: Data) -> NavvoJSON? {
        guard !data.isEmpty else { return nil }
        guard
            let object = try? JSONSerialization.jsonObject(
                with: data, options: [.fragmentsAllowed]
            )
        else { return nil }
        return NavvoJSON(jsonObject: object)
    }

    /// Serialize back to JSON bytes.
    public func encodedData() throws -> Data {
        try JSONSerialization.data(
            withJSONObject: jsonObjectValue, options: [.fragmentsAllowed]
        )
    }

    /// The value as a Foundation JSON object graph.
    public var jsonObjectValue: Any {
        switch self {
        case .null: return NSNull()
        case .bool(let bool): return bool
        case .number(let number): return number
        case .string(let string): return string
        case .array(let array): return array.map { $0.jsonObjectValue }
        case .object(let object): return object.mapValues { $0.jsonObjectValue }
        }
    }
}
