import CoreLocation
import Foundation

/// Where the rider is.
///
/// `isReal` separates a genuine fix from the fallback. Callers that only need a map centre or a
/// search origin can ignore it; anything that makes a decision about the physical world (ending a
/// ride inside a zone) should not, because acting on the fallback would be acting on a position
/// the rider is nowhere near.
struct Position: Equatable {
    let lat: Double
    let lng: Double
    let isReal: Bool
}

/// The single answer to "where is this phone", shared by the map and every view model.
///
/// Mirrors `LocationProvider.kt` on Android, including its degrade-never-fail contract: ask for
/// authorization if it has not been asked, wait a bounded time for a fix, and otherwise hand back
/// the last real position this process saw, or Riyadh. A rider with location switched off still
/// gets a working screen, just not a local one.
///
/// `CLLocationManager` reports through a delegate rather than async/await, so the bridge is a set
/// of continuations resumed from the delegate callbacks. Every waiter is resumed exactly once:
/// either by a callback or by the timeout, whichever comes first.
final class LocationProvider: NSObject {

    /// Riyadh centre: the fallback when there is no permission and no fix to be had.
    static let fallbackLat = 24.7136
    static let fallbackLng = 46.6753
    static let fallback = Position(lat: fallbackLat, lng: fallbackLng, isReal: false)

    /// Long enough for a cold GPS start, short enough that the home screen never waits on it.
    private static let fixTimeoutNanos: UInt64 = 4_000_000_000

    private let manager = CLLocationManager()
    private let lock = NSLock()

    /// The last real fix, kept for the lifetime of the process. An active fix can take seconds,
    /// and a rider ending a ride in a basement car park may get none at all: a minute-old position
    /// from the same trip beats Riyadh.
    private var remembered: Position?

    private var fixWaiters: [CheckedContinuation<Position?, Never>] = []
    private var authWaiters: [CheckedContinuation<Bool, Never>] = []

    override init() {
        super.init()
        manager.delegate = self
        // Hundred-metre accuracy: this feeds a nearby search over hundreds of metres, so paying
        // for precision the query cannot use would only cost battery.
        manager.desiredAccuracy = kCLLocationAccuracyHundredMeters
    }

    var hasPermission: Bool {
        switch manager.authorizationStatus {
        case .authorizedWhenInUse, .authorizedAlways: return true
        default: return false
        }
    }

    /// The last real fix this process saw, or nil if there has not been one.
    func lastKnown() -> Position? {
        lock.lock()
        defer { lock.unlock() }
        return remembered
    }

    /// The rider's position now, degrading rather than failing.
    func current() async -> Position {
        if manager.authorizationStatus == .notDetermined {
            // The first call happens while the system prompt is still on screen, so wait for the
            // rider's answer instead of reporting Riyadh and then never asking again.
            _ = await requestAuthorization()
        }
        guard hasPermission else { return lastKnown() ?? Self.fallback }

        // The delegate is what remembers the fix, so there is nothing to store here.
        if let fix = await requestFix() {
            return fix
        }
        return lastKnown() ?? Self.fallback
    }

    private func requestAuthorization() async -> Bool {
        await withCheckedContinuation { (continuation: CheckedContinuation<Bool, Never>) in
            lock.lock()
            authWaiters.append(continuation)
            lock.unlock()
            manager.requestWhenInUseAuthorization()
            Task { [weak self] in
                try? await Task.sleep(nanoseconds: Self.fixTimeoutNanos)
                self?.resumeAuthWaiters(with: self?.hasPermission ?? false)
            }
        }
    }

    private func requestFix() async -> Position? {
        await withCheckedContinuation { (continuation: CheckedContinuation<Position?, Never>) in
            lock.lock()
            fixWaiters.append(continuation)
            lock.unlock()
            manager.requestLocation()
            Task { [weak self] in
                try? await Task.sleep(nanoseconds: Self.fixTimeoutNanos)
                self?.resumeFixWaiters(with: nil)
            }
        }
    }

    /// Drains the waiter list under the lock, so a continuation can never be resumed twice even
    /// when a delegate callback and the timeout land together.
    private func resumeFixWaiters(with position: Position?) {
        lock.lock()
        let waiting = fixWaiters
        fixWaiters = []
        lock.unlock()
        waiting.forEach { $0.resume(returning: position) }
    }

    private func resumeAuthWaiters(with granted: Bool) {
        lock.lock()
        let waiting = authWaiters
        authWaiters = []
        lock.unlock()
        waiting.forEach { $0.resume(returning: granted) }
    }
}

extension LocationProvider: CLLocationManagerDelegate {

    func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
        guard manager.authorizationStatus != .notDetermined else { return }
        resumeAuthWaiters(with: hasPermission)
    }

    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        guard let last = locations.last else {
            resumeFixWaiters(with: nil)
            return
        }
        let fix = Position(
            lat: last.coordinate.latitude,
            lng: last.coordinate.longitude,
            isReal: true
        )
        // Remembered here rather than only in `current()`, so a fix that lands after its caller
        // gave up is still the answer the next caller gets.
        lock.lock()
        remembered = fix
        lock.unlock()
        resumeFixWaiters(with: fix)
    }

    func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
        // A failed fix is not an error the rider should see: the caller falls back.
        resumeFixWaiters(with: nil)
    }
}
