//
//  Models.swift
//  Navvo Maps SDK for iOS
//
//  Typed response models for the services client. Each model parses the
//  common fields into typed accessors while keeping the full payload on
//  `raw` for forward-compatibility; shapes too variable to usefully type are
//  returned as `NavvoJSON` directly by the client.
//

import Foundation

// MARK: - Style catalog

/// One entry from the `GET /tiles/styles` catalog.
public struct NavvoStyleInfo: Codable, Equatable, Sendable {
    /// The style id (e.g. `standard`, `dark`).
    public let style: String

    /// A human label.
    public let label: String?

    /// The category (`base` / `imagery`).
    public let category: String?

    /// A short description.
    public let description: String?

    /// Create a style entry.
    public init(style: String, label: String? = nil, category: String? = nil, description: String? = nil) {
        self.style = style
        self.label = label
        self.category = category
        self.description = description
    }

    /// Parse a raw catalog entry; `nil` when the id is missing.
    public init?(json: NavvoJSON) {
        guard let style = json["style"]?.stringValue, !style.isEmpty else { return nil }
        self.init(
            style: style,
            label: json["label"]?.stringValue,
            category: json["category"]?.stringValue,
            description: json["description"]?.stringValue
        )
    }
}

// MARK: - Search & geocode

/// A single place returned by ``SearchResult``.
public struct SearchPlace: Codable, Sendable {
    /// The full untyped item.
    public let raw: NavvoJSON

    /// Wrap a raw search item.
    public init(raw: NavvoJSON) { self.raw = raw }

    public init(from decoder: Decoder) throws { raw = try NavvoJSON(from: decoder) }
    public func encode(to encoder: Encoder) throws { try raw.encode(to: encoder) }

    /// Stable slug (for `place(slug)`).
    public var slug: String? { raw["slug"]?.stringValue }

    /// English display name.
    public var name: String? { raw["nameEn"]?.stringValue ?? raw["name"]?.stringValue }

    /// Arabic display name.
    public var nameAr: String? { raw["nameAr"]?.stringValue }

    /// Canonical category slug.
    public var category: String? {
        raw["canonicalCategorySlug"]?.stringValue ?? raw["category"]?.stringValue
    }

    /// Rating, when present.
    public var rating: Double? { raw["rating"]?.doubleValue }

    /// Parsed WKT `center` (`POINT(lng lat)`), when present.
    public var location: LatLng? { LatLng(wkt: raw["center"]?.stringValue) }
}

/// A `GET /search` response.
public struct SearchResult: Codable, Sendable {
    /// The full untyped payload.
    public let raw: NavvoJSON

    /// Wrap a raw search payload.
    public init(raw: NavvoJSON) { self.raw = raw }

    public init(from decoder: Decoder) throws { raw = try NavvoJSON(from: decoder) }
    public func encode(to encoder: Encoder) throws { try raw.encode(to: encoder) }

    /// The matched places (`places.items`).
    public var places: [SearchPlace] {
        guard let items = raw["places"]?["items"]?.arrayValue else { return [] }
        return items.map { SearchPlace(raw: $0) }
    }

    /// Related suggestions, when the search found no direct matches.
    public var suggestions: [SearchPlace] {
        guard let items = raw["suggestions"]?.arrayValue else { return [] }
        return items.map { SearchPlace(raw: $0) }
    }

    /// Total number of matches, when reported.
    public var total: Int? { raw["places"]?["total"]?.intValue }
}

/// One forward/reverse geocoding hit (string coordinates parsed to doubles).
public struct GeocodeResult: Codable, Sendable {
    /// The full untyped payload.
    public let raw: NavvoJSON

    /// Wrap a raw geocoder row.
    public init(raw: NavvoJSON) { self.raw = raw }

    public init(from decoder: Decoder) throws { raw = try NavvoJSON(from: decoder) }
    public func encode(to encoder: Encoder) throws { try raw.encode(to: encoder) }

    /// Latitude, parsed from the (string) `lat`.
    public var latitude: Double? { raw["lat"]?.doubleValue }

    /// Longitude, parsed from the (string) `lon`/`lng`.
    public var longitude: Double? { raw["lon"]?.doubleValue ?? raw["lng"]?.doubleValue }

    /// A short name for the hit.
    public var name: String? { raw["name"]?.stringValue }

    /// The full display name / formatted address.
    public var displayName: String? {
        raw["display_name"]?.stringValue ?? raw["displayName"]?.stringValue
    }

    /// The result category (e.g. `boundary`).
    public var category: String? { raw["category"]?.stringValue }

    /// The result type (e.g. `administrative`).
    public var type: String? { raw["type"]?.stringValue }

    /// The hit as a ``LatLng``, or `nil` when coordinates are missing.
    public var location: LatLng? {
        guard let latitude, let longitude else { return nil }
        return LatLng(latitude, longitude)
    }

    /// The bounding box (`boundingbox` is `[south, north, west, east]`).
    public var boundingBox: LatLngBounds? {
        guard
            let box = (raw["boundingbox"] ?? raw["boundingBox"])?.arrayValue,
            box.count == 4,
            let south = box[0].doubleValue,
            let north = box[1].doubleValue,
            let west = box[2].doubleValue,
            let east = box[3].doubleValue
        else { return nil }
        return LatLngBounds(
            southwest: LatLng(south, west),
            northeast: LatLng(north, east)
        )
    }
}

// MARK: - Places

/// A place — a list item or a full detail record.
public struct PlaceDetails: Codable, Sendable {
    /// The full untyped payload.
    public let raw: NavvoJSON

    /// Wrap a raw place payload.
    public init(raw: NavvoJSON) { self.raw = raw }

    public init(from decoder: Decoder) throws { raw = try NavvoJSON(from: decoder) }
    public func encode(to encoder: Encoder) throws { try raw.encode(to: encoder) }

    /// Stable id.
    public var id: String? { raw["id"]?.stringValue }

    /// URL slug.
    public var slug: String? { raw["slug"]?.stringValue }

    /// English name.
    public var nameEn: String? { raw["nameEn"]?.stringValue }

    /// Arabic name.
    public var nameAr: String? { raw["nameAr"]?.stringValue }

    /// Canonical category slug.
    public var category: String? { raw["canonicalCategorySlug"]?.stringValue }

    /// Rating (normalised to `Double`; the API sends a string on detail).
    public var rating: Double? { raw["rating"]?.doubleValue }

    /// Human open-state text (e.g. "Open · until 23:00"), when present.
    public var openText: String? {
        raw["openText"]?.stringValue
            ?? raw["openState"]?.stringValue
            ?? raw["open_text"]?.stringValue
    }

    /// Parsed WKT `center` (`POINT(lng lat)`).
    public var center: LatLng? { LatLng(wkt: raw["center"]?.stringValue) }

    /// The recommended entrance to route to (arrival intelligence), or `nil`.
    ///
    /// Prefer `place.recommendedRouteTarget ?? place.center` — route to the
    /// door, not the pin.
    public var recommendedRouteTarget: LatLng? {
        guard
            let candidate = raw["arrivalIntelligence"]?["candidates"]?[0],
            let target = candidate["routeTarget"],
            let lat = target["lat"]?.doubleValue,
            let lng = target["lng"]?.doubleValue ?? target["lon"]?.doubleValue
        else { return nil }
        return LatLng(lat, lng)
    }
}

/// One page of `GET /places` results.
public struct PlacesPage: Codable, Sendable {
    /// The full untyped payload.
    public let raw: NavvoJSON

    /// Wrap a raw page payload.
    public init(raw: NavvoJSON) { self.raw = raw }

    public init(from decoder: Decoder) throws { raw = try NavvoJSON(from: decoder) }
    public func encode(to encoder: Encoder) throws { try raw.encode(to: encoder) }

    /// The place items on this page.
    public var items: [PlaceDetails] {
        guard let list = raw["items"]?.arrayValue else { return [] }
        return list.map { PlaceDetails(raw: $0) }
    }

    /// Total matches across all pages.
    public var total: Int { raw["total"]?.intValue ?? 0 }

    /// The current page number (1-based).
    public var page: Int { raw["page"]?.intValue ?? 1 }

    /// The page size.
    public var limit: Int { raw["limit"]?.intValue ?? 0 }

    /// Whether another page exists (`page * limit < total` — no cursors).
    public var hasNextPage: Bool { page * limit < total }
}

/// A `POST /places/reachable` result.
public struct ReachableResult: Codable, Sendable {
    /// The full untyped payload.
    public let raw: NavvoJSON

    /// Wrap a raw reachable payload.
    public init(raw: NavvoJSON) { self.raw = raw }

    public init(from decoder: Decoder) throws { raw = try NavvoJSON(from: decoder) }
    public func encode(to encoder: Encoder) throws { try raw.encode(to: encoder) }

    /// The reachable places, untyped.
    public var places: [NavvoJSON] { raw["places"]?.arrayValue ?? [] }
}

// MARK: - Routing

/// A `POST /routing/plan` result.
///
/// `/routing/plan` nests the routing-engine envelope under the `engine` key;
/// the trip is read from `engine.data.trip`. Convenience accessors decode the
/// polyline6 leg shapes into a path and expose the summary.
public struct RoutePlan: Codable, Sendable {
    /// The full untyped envelope.
    public let raw: NavvoJSON

    /// Wrap a raw plan payload.
    public init(raw: NavvoJSON) { self.raw = raw }

    public init(from decoder: Decoder) throws { raw = try NavvoJSON(from: decoder) }
    public func encode(to encoder: Encoder) throws { try raw.encode(to: encoder) }

    /// The raw engine `trip` object (`engine.data.trip`), or `nil`.
    public var trip: NavvoJSON? {
        let envelope = raw["engine"] ?? raw
        guard let trip = envelope["data"]?["trip"], trip.objectValue != nil else { return nil }
        return trip
    }

    /// The engine `summary` block, when present.
    public var summary: NavvoJSON? {
        guard let summary = trip?["summary"], summary.objectValue != nil else { return nil }
        return summary
    }

    /// Total distance in kilometres (`/plan` always returns km).
    public var distanceKm: Double? { summary?["length"]?.doubleValue }

    /// Total duration in minutes.
    public var durationMinutes: Double? {
        guard let seconds = summary?["time"]?.doubleValue else { return nil }
        return seconds / 60.0
    }

    /// The AI labels block (empty when `includeLabels: false`).
    public var labels: NavvoJSON? {
        guard let labels = raw["labels"], labels.objectValue != nil else { return nil }
        return labels
    }

    /// The logistics-zone block, when present.
    public var logistics: NavvoJSON? {
        guard let logistics = raw["logistics"], logistics.objectValue != nil else { return nil }
        return logistics
    }

    /// The engine legs (`type`/`shape`/`maneuvers`), untyped.
    public var legs: [NavvoJSON] { trip?["legs"]?.arrayValue ?? [] }

    /// Flattened maneuvers across all legs.
    public var maneuvers: [NavvoJSON] {
        var output: [NavvoJSON] = []
        for leg in legs {
            if let list = leg["maneuvers"]?.arrayValue {
                output.append(contentsOf: list)
            }
        }
        return output
    }

    /// Every leg's polyline6 `shape` decoded into a single path.
    public var geometry: [LatLng] {
        var points: [LatLng] = []
        for leg in legs {
            if let shape = leg["shape"]?.stringValue, !shape.isEmpty {
                points.append(contentsOf: NavvoPolylineCodec.decode(shape))
            }
        }
        return points
    }

    /// The optimised stop order (`locations[].original_index`), when
    /// `optimize` reordered the stops.
    public var stopOrder: [Int] {
        guard let locations = trip?["locations"]?.arrayValue else { return [] }
        return locations.compactMap { $0["original_index"]?.intValue }
    }
}

/// A `POST /routing/route` result (flat; `data.trip`, no `engine` key).
public struct RouteResult: Codable, Sendable {
    /// The full untyped payload.
    public let raw: NavvoJSON

    /// Wrap a raw route payload.
    public init(raw: NavvoJSON) { self.raw = raw }

    public init(from decoder: Decoder) throws { raw = try NavvoJSON(from: decoder) }
    public func encode(to encoder: Encoder) throws { try raw.encode(to: encoder) }

    /// The engine `trip` object (`data.trip`), or `nil`.
    public var trip: NavvoJSON? {
        guard let trip = raw["data"]?["trip"], trip.objectValue != nil else { return nil }
        return trip
    }

    /// The engine `summary` block, when present.
    public var summary: NavvoJSON? {
        guard let summary = trip?["summary"], summary.objectValue != nil else { return nil }
        return summary
    }

    /// The AI labels block, when present.
    public var labels: NavvoJSON? {
        guard let labels = raw["labels"], labels.objectValue != nil else { return nil }
        return labels
    }

    /// The engine legs, untyped.
    public var legs: [NavvoJSON] { trip?["legs"]?.arrayValue ?? [] }

    /// Every leg's polyline6 `shape` decoded into a single path.
    public var geometry: [LatLng] {
        var points: [LatLng] = []
        for leg in legs {
            if let shape = leg["shape"]?.stringValue, !shape.isEmpty {
                points.append(contentsOf: NavvoPolylineCodec.decode(shape))
            }
        }
        return points
    }
}

/// A `POST /traffic/route` result (flat, with congestion segments).
public struct TrafficRoute: Codable, Sendable {
    /// The full untyped payload.
    public let raw: NavvoJSON

    /// Wrap a raw traffic-route payload.
    public init(raw: NavvoJSON) { self.raw = raw }

    public init(from decoder: Decoder) throws { raw = try NavvoJSON(from: decoder) }
    public func encode(to encoder: Encoder) throws { try raw.encode(to: encoder) }

    /// Live ETA in minutes, when reported.
    public var durationMinutes: Double? {
        if let seconds = raw["durationSec"]?.doubleValue ?? raw["duration"]?.doubleValue {
            return seconds / 60.0
        }
        return raw["durationMin"]?.doubleValue
    }

    /// Extra delay vs free-flow, in minutes, when reported.
    public var delayMinutes: Double? {
        if let seconds = raw["delaySec"]?.doubleValue ?? raw["delay"]?.doubleValue {
            return seconds / 60.0
        }
        return raw["delayMin"]?.doubleValue
    }

    /// Congestion-coloured segments, untyped.
    public var segments: [NavvoJSON] { raw["segments"]?.arrayValue ?? [] }
}

/// A `POST /routing/matrix` result with a convenience cell accessor.
public struct TravelMatrix: Codable, Sendable {
    /// The full untyped payload.
    public let raw: NavvoJSON

    /// Wrap a raw matrix payload (accepts the `data` envelope or a bare object).
    public init(raw: NavvoJSON) { self.raw = raw }

    public init(from decoder: Decoder) throws { raw = try NavvoJSON(from: decoder) }
    public func encode(to encoder: Encoder) throws { try raw.encode(to: encoder) }

    private var grid: [NavvoJSON] {
        let data = raw["data"] ?? raw
        return data["sources_to_targets"]?.arrayValue ?? []
    }

    /// The `{time, distance, ...}` cell from source `i` to target `j`, or
    /// `nil` when out of range or the pair is unreachable.
    public func cell(source: Int, target: Int) -> NavvoJSON? {
        let rows = grid
        guard rows.indices.contains(source),
              let row = rows[source].arrayValue,
              row.indices.contains(target)
        else { return nil }
        let cell = row[target]
        return cell.objectValue != nil ? cell : nil
    }

    /// Travel time in seconds from source `i` to target `j`, or `nil`.
    public func timeSeconds(source: Int, target: Int) -> Double? {
        cell(source: source, target: target)?["time"]?.doubleValue
    }

    /// Distance in kilometres from source `i` to target `j`, or `nil`.
    public func distanceKm(source: Int, target: Int) -> Double? {
        cell(source: source, target: target)?["distance"]?.doubleValue
    }
}

/// A `POST /routing/isochrone` (or `/places/reachable`) GeoJSON result.
public struct IsochroneResult: Codable, Sendable {
    /// The GeoJSON `FeatureCollection` (positions are `[lng, lat]`) — pass to
    /// `NavvoMapView.addGeoJSON` as-is.
    public let geoJSON: NavvoJSON

    /// Wrap a GeoJSON payload.
    public init(geoJSON: NavvoJSON) { self.geoJSON = geoJSON }

    public init(from decoder: Decoder) throws { geoJSON = try NavvoJSON(from: decoder) }
    public func encode(to encoder: Encoder) throws { try geoJSON.encode(to: encoder) }

    /// The GeoJSON features, untyped.
    public var features: [NavvoJSON] { geoJSON["features"]?.arrayValue ?? [] }
}
