import Foundation

/// Why the rider cannot end here, plus the closest spot that is open right now.
struct ParkingGuidance {
    let parkingRequired: Bool
    let nearest: ParkingSpot?
}

/// Geofence facts a rider needs: where they may park, and whether they may end here.
///
/// Separate from `RideRepository` because it talks to a different service and is read-only. A
/// failure here must never stop a ride: the map simply shows no spots, and the end refusal falls
/// back to its generic wording.
struct ZoneRepository {
    let client: APIClient
    let config: APIConfig

    func parkingNearby(lat: Double, lng: Double, radiusMeters: Double = 3000) async throws -> [ParkingSpot] {
        var components = URLComponents(
            url: config.zone.appendingPathComponent("api/v1/zones/parking/nearby"),
            resolvingAgainstBaseURL: false
        )!
        components.queryItems = [
            URLQueryItem(name: "lat", value: String(lat)),
            URLQueryItem(name: "lng", value: String(lng)),
            URLQueryItem(name: "radiusMeters", value: String(radiusMeters)),
        ]
        return try await client.get(components.url!)
    }

    func evaluate(lat: Double, lng: Double) async throws -> ZoneEvaluation {
        var components = URLComponents(
            url: config.zone.appendingPathComponent("api/v1/zones/evaluate"),
            resolvingAgainstBaseURL: false
        )!
        components.queryItems = [
            URLQueryItem(name: "lat", value: String(lat)),
            URLQueryItem(name: "lng", value: String(lng)),
        ]
        return try await client.get(components.url!)
    }

    /// Why an end was refused here, and where to go instead.
    ///
    /// Returns nil when zone-service cannot be reached: the caller then keeps its generic message
    /// rather than inventing a reason it does not know.
    func endRefusalGuidance(lat: Double, lng: Double) async -> ParkingGuidance? {
        do {
            let evaluation = try await evaluate(lat: lat, lng: lng)
            guard evaluation.parkingRequired else {
                return ParkingGuidance(parkingRequired: false, nearest: nil)
            }
            let spots = try await parkingNearby(lat: lat, lng: lng, radiusMeters: 5000)
            return ParkingGuidance(parkingRequired: true, nearest: spots.first { $0.openNow })
        } catch {
            return nil
        }
    }
}

/// The spot name to show this rider.
///
/// The server sends both languages rather than picking one, because it has no reliable way to
/// know what the phone is set to. Choosing here means a rider who switches the device to Arabic
/// sees Arabic immediately, with no round trip.
///
/// Falls back to the other language rather than showing nothing: an operator who filled in only
/// one name still gets a usable pin, and a nameless spot returns nil so the caller can say
/// "a parking spot" instead of printing an empty string.
func parkingDisplayName(englishName: String?, arabicName: String?) -> String? {
    // preferredLocalizations, not Locale.preferredLanguages: the former is the language this app
    // actually resolved to, the latter can name one the app does not ship, which would show
    // English names on a screen full of Arabic.
    let isArabic = Bundle.main.preferredLocalizations.first?.hasPrefix("ar") ?? false
    let preferred = isArabic ? arabicName : englishName
    let fallback = isArabic ? englishName : arabicName
    if let preferred, !preferred.isEmpty { return preferred }
    if let fallback, !fallback.isEmpty { return fallback }
    return nil
}

extension ParkingSpot {
    var displayName: String? { parkingDisplayName(englishName: riderNameEn, arabicName: riderNameAr) }
}

extension MatchedParking {
    var displayName: String? { parkingDisplayName(englishName: riderNameEn, arabicName: riderNameAr) }
}

/// The fee as a rider reads it: always two decimals, so "2.5" never appears next to "2.50".
///
/// Formatted with a fixed POSIX locale on purpose. It is interpolated into an already-localised
/// sentence that supplies the currency word, and using the device locale here would produce
/// Arabic-Indic digits inside a template whose other number (the distance) is Western.
func formatParkingFee(_ fee: Double) -> String {
    String(format: "%.2f", fee)
}
