//
//  LocationMapsService.swift
//  Confab
//
//  Read-only Location and Maps integration via CoreLocation and MapKit
//
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
// Copyright (C) 2026 Rune Olsen
//
// This file is part of Confab.
//
// Confab is licensed under the PolyForm Noncommercial License 1.0.0.
// You may use, study, modify, and share it for any noncommercial
// purpose. Commercial use — including selling Confab or any part of
// it, standalone or bundled into another product or service —
// requires a separate commercial license from the copyright holder.
//
// See the LICENSE file or
// <https://polyformproject.org/licenses/noncommercial/1.0.0> for
// the full license text. For commercial licensing, contact Rune
// Olsen via <https://confab.no>.


import CoreLocation
import Foundation
import MapKit
import os

@Observable
class LocationMapsService: NSObject, CLLocationManagerDelegate {
    static let shared = LocationMapsService()

    private let locationManager = CLLocationManager()

    private var authContinuation: CheckedContinuation<Bool, Never>?
    private var locationContinuation: CheckedContinuation<CLLocation?, Never>?

    private override init() {
        super.init()
        locationManager.delegate = self
    }

    // MARK: - Authorization

    var authStatus: CLAuthorizationStatus {
        locationManager.authorizationStatus
    }

    var authorized: Bool {
        authStatus == .authorizedAlways || authStatus == .authorized
    }

    var accessState: PersonalDataAccessState {
        let status = authStatus
        Log.mcp.debug("LocationMapsService.accessState -> status=\(Self.describe(status)) (raw=\(status.rawValue))")
        switch status {
        case .authorizedAlways, .authorized: return .granted
        case .notDetermined: return .notDetermined
        default: return .denied
        }
    }

    @discardableResult
    func requestAccess() async -> Bool {
        let before = locationManager.authorizationStatus
        Log.mcp.info("LocationMapsService.requestAccess: status before = \(Self.describe(before)) (raw=\(before.rawValue))")
        if before != .notDetermined {
            Log.mcp.info("LocationMapsService.requestAccess: skipping OS prompt (not notDetermined)")
            return authorized
        }
        return await withCheckedContinuation { continuation in
            self.authContinuation = continuation
            locationManager.requestWhenInUseAuthorization()
        }
    }

    func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
        let status = manager.authorizationStatus
        Log.mcp.info("LocationMapsService: authorization changed -> \(Self.describe(status)) (raw=\(status.rawValue))")
        authContinuation?.resume(returning: authorized)
        authContinuation = nil
    }

    nonisolated static func describe(_ status: CLAuthorizationStatus) -> String {
        switch status {
        case .notDetermined: return "notDetermined"
        case .restricted: return "restricted"
        case .denied: return "denied"
        case .authorizedAlways: return "authorizedAlways"
        case .authorized: return "authorized"
        @unknown default: return "unknown"
        }
    }

    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        locationContinuation?.resume(returning: locations.last)
        locationContinuation = nil
    }

    func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
        Log.mcp.error("Location request failed: \(error.localizedDescription)")
        locationContinuation?.resume(returning: nil)
        locationContinuation = nil
    }

    private func currentLocation() async -> CLLocation? {
        await withCheckedContinuation { continuation in
            self.locationContinuation = continuation
            locationManager.requestLocation()
        }
    }

    // MARK: - Tool Schemas

    func getToolSchemas() -> [Tool] {
        [
            makeTool(
                name: "location_get_current",
                description: "Get the device's current location, including a human-readable address.",
                properties: [:],
                required: []
            ),
            makeTool(
                name: "maps_search_places",
                description: "Search for places (businesses, landmarks, addresses) by name or category.",
                properties: [
                    "query": prop("string", "What to search for, e.g. 'coffee shops' or 'Eiffel Tower'"),
                    "near": prop("string", "Optional: an address or 'latitude,longitude' to search near")
                ],
                required: ["query"]
            ),
            makeTool(
                name: "maps_geocode",
                description: "Convert an address into geographic coordinates and a formatted address.",
                properties: [
                    "address": prop("string", "The address to geocode")
                ],
                required: ["address"]
            ),
            makeTool(
                name: "maps_get_directions",
                description: "Get distance and estimated travel time between two locations.",
                properties: [
                    "origin": prop("string", "Starting address or 'latitude,longitude'"),
                    "destination": prop("string", "Destination address or 'latitude,longitude'"),
                    "transport_type": prop("string", "Mode of transport", enumValues: ["driving", "walking", "transit"])
                ],
                required: ["origin", "destination"]
            )
        ]
    }

    // MARK: - Tool Execution

    func executeTool(name: String, arguments: String) async -> [String: Any] {
        Log.mcp.info("Executing LocationMaps tool: \(name)")
        let args = parseArgs(arguments)

        switch name {
        case "location_get_current":
            guard authorized else { return ["error": "Location permission not granted. Grant access in Settings > MCP."] }
            return await getCurrentLocation()

        case "maps_search_places":
            guard let query = args["query"] as? String, !query.isEmpty else {
                return ["error": "Missing required parameter: query"]
            }
            let near = args["near"] as? String
            return await searchPlaces(query: query, near: near)

        case "maps_geocode":
            guard let address = args["address"] as? String, !address.isEmpty else {
                return ["error": "Missing required parameter: address"]
            }
            return await geocode(address: address)

        case "maps_get_directions":
            guard let origin = args["origin"] as? String, let destination = args["destination"] as? String else {
                return ["error": "Missing required parameter: origin and/or destination"]
            }
            let transportType = args["transport_type"] as? String ?? "driving"
            return await getDirections(origin: origin, destination: destination, transportType: transportType)

        default:
            return ["error": "Unknown LocationMaps tool: \(name)"]
        }
    }

    // MARK: - Implementations

    private func getCurrentLocation() async -> [String: Any] {
        guard let location = await currentLocation() else {
            return ["error": "Could not determine current location"]
        }
        var result: [String: Any] = [
            "latitude": location.coordinate.latitude,
            "longitude": location.coordinate.longitude
        ]
        if let mapItem = await reverseGeocode(location), let address = addressString(for: mapItem) {
            result["address"] = address
        }
        return result
    }

    private func searchPlaces(query: String, near: String?) async -> [String: Any] {
        let request = MKLocalSearch.Request()
        request.naturalLanguageQuery = query
        if let near, let coordinate = await coordinate(for: near) {
            request.region = MKCoordinateRegion(center: coordinate, latitudinalMeters: 20_000, longitudinalMeters: 20_000)
        }

        do {
            let response = try await MKLocalSearch(request: request).start()
            let places = response.mapItems.prefix(15).map { item -> [String: Any] in
                var place: [String: Any] = ["name": item.name ?? "Unknown"]
                place["latitude"] = item.location.coordinate.latitude
                place["longitude"] = item.location.coordinate.longitude
                if let address = addressString(for: item) {
                    place["address"] = address
                }
                if let phone = item.phoneNumber { place["phone"] = phone }
                return place
            }
            return ["count": places.count, "places": Array(places)]
        } catch {
            return ["error": "Search failed: \(error.localizedDescription)"]
        }
    }

    private func geocode(address: String) async -> [String: Any] {
        guard let request = MKGeocodingRequest(addressString: address) else {
            return ["error": "Invalid address: \(address)"]
        }
        do {
            guard let mapItem = try await request.mapItems.first else {
                return ["error": "No results found for address: \(address)"]
            }
            var result: [String: Any] = [
                "latitude": mapItem.location.coordinate.latitude,
                "longitude": mapItem.location.coordinate.longitude
            ]
            if let formatted = addressString(for: mapItem) {
                result["formatted_address"] = formatted
            }
            return result
        } catch {
            return ["error": "Geocoding failed: \(error.localizedDescription)"]
        }
    }

    private func getDirections(origin: String, destination: String, transportType: String) async -> [String: Any] {
        guard let originCoordinate = await coordinate(for: origin) else {
            return ["error": "Could not resolve origin: \(origin)"]
        }
        guard let destinationCoordinate = await coordinate(for: destination) else {
            return ["error": "Could not resolve destination: \(destination)"]
        }

        let request = MKDirections.Request()
        request.source = MKMapItem(location: CLLocation(latitude: originCoordinate.latitude, longitude: originCoordinate.longitude), address: nil)
        request.destination = MKMapItem(location: CLLocation(latitude: destinationCoordinate.latitude, longitude: destinationCoordinate.longitude), address: nil)
        switch transportType {
        case "walking": request.transportType = .walking
        case "transit": request.transportType = .transit
        default: request.transportType = .automobile
        }

        do {
            let response = try await MKDirections(request: request).calculate()
            guard let route = response.routes.first else {
                return ["error": "No route found"]
            }
            let distanceFormatter = MKDistanceFormatter()
            let durationFormatter = DateComponentsFormatter()
            durationFormatter.allowedUnits = [.hour, .minute]
            durationFormatter.unitsStyle = .short

            return [
                "distance_meters": route.distance,
                "distance_text": distanceFormatter.string(fromDistance: route.distance),
                "duration_seconds": route.expectedTravelTime,
                "duration_text": durationFormatter.string(from: route.expectedTravelTime) ?? "",
                "transport_type": transportType
            ]
        } catch {
            return ["error": "Directions failed: \(error.localizedDescription)"]
        }
    }

    // MARK: - Helpers

    private func coordinate(for text: String) async -> CLLocationCoordinate2D? {
        let parts = text.split(separator: ",").map { $0.trimmingCharacters(in: .whitespaces) }
        if parts.count == 2, let lat = Double(parts[0]), let lon = Double(parts[1]) {
            return CLLocationCoordinate2D(latitude: lat, longitude: lon)
        }
        guard let request = MKGeocodingRequest(addressString: text) else { return nil }
        if let mapItems = try? await request.mapItems, let mapItem = mapItems.first {
            return mapItem.location.coordinate
        }
        return nil
    }

    private func reverseGeocode(_ location: CLLocation) async -> MKMapItem? {
        guard let request = MKReverseGeocodingRequest(location: location) else { return nil }
        let mapItems = try? await request.mapItems
        return mapItems?.first
    }

    private func addressString(for mapItem: MKMapItem) -> String? {
        mapItem.address?.fullAddress
            ?? mapItem.addressRepresentations?.fullAddress(includingRegion: true, singleLine: true)
    }

    private func parseArgs(_ arguments: String) -> [String: Any] {
        guard let data = arguments.data(using: .utf8),
              let dict = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
            return [:]
        }
        return dict
    }

    private func makeTool(name: String, description: String, properties: [String: Tool.Function.Parameters.Property], required: [String]) -> Tool {
        Tool(
            type: "function",
            function: Tool.Function(
                name: name,
                description: description,
                parameters: Tool.Function.Parameters(
                    type: "object",
                    properties: properties,
                    required: required
                )
            )
        )
    }

    private func prop(_ type: String, _ description: String, enumValues: [String]? = nil) -> Tool.Function.Parameters.Property {
        Tool.Function.Parameters.Property(type: type, description: description, enum: enumValues)
    }
}
