import Foundation
import SwiftData
import CoreLocation

/// Convenience wrapper around the SwiftData ModelContext for
/// LoggedFlight CRUD + airframe metadata caching. View code talks to
/// this rather than poking ModelContext directly so we have a single
/// place to enforce dedupe rules, derive computed fields, etc.
@MainActor
final class FlightHistoryStore {
    let context: ModelContext
    private let airportDatabase: AirportDatabase

    init(context: ModelContext, airportDatabase: AirportDatabase) {
        self.context = context
        self.airportDatabase = airportDatabase
    }

    // MARK: - Persistence helper

    /// Persist any pending mutations on the underlying ``ModelContext``.
    /// Any thrown error is surfaced via ``DataIntegrityMonitor`` so the
    /// user sees a banner about the failure instead of silently losing
    /// data. Returns true on success so call sites can act on failure
    /// (e.g. avoid clearing a draft).
    ///
    /// `operation` is a short verb describing what was being saved
    /// ("save flight", "delete flight", "update standby outcome"). It
    /// appears in the banner so the user can correlate the failure with
    /// their last action.
    @discardableResult
    func persist(_ operation: String) -> Bool {
        do {
            try context.save()
            return true
        } catch {
            DataIntegrityMonitor.shared.reportSaveFailure(operation, error: error)
            return false
        }
    }

    // MARK: - LoggedFlight CRUD

    /// Save a new flight. No dedupe logic here — callers (importers)
    /// own that. Direct user adds always create a fresh record.
    @discardableResult
    func save(_ flight: LoggedFlight) -> LoggedFlight {
        context.insert(flight)
        persist("save flight")
        return flight
    }

    func delete(_ flight: LoggedFlight) {
        context.delete(flight)
        persist("delete flight")
    }

    /// Returns true if a flight with the same date + flight number +
    /// route already exists. Used by importers to skip dupes.
    func exists(flightDate: Date, flightLabel: String, departureIATA: String, arrivalIATA: String) -> Bool {
        let day = Calendar.current.startOfDay(for: flightDate)
        let next = Calendar.current.date(byAdding: .day, value: 1, to: day) ?? day
        let predicate = #Predicate<LoggedFlight> { f in
            f.flightDate >= day && f.flightDate < next
                && f.departureIATA == departureIATA
                && f.arrivalIATA == arrivalIATA
        }
        let descriptor = FetchDescriptor<LoggedFlight>(predicate: predicate)
        let matches = (try? context.fetch(descriptor)) ?? []
        return matches.contains { f in
            f.flightLabel.uppercased() == flightLabel.uppercased()
        }
    }

    func allFlights() -> [LoggedFlight] {
        let descriptor = FetchDescriptor<LoggedFlight>(
            sortBy: [SortDescriptor(\.flightDate, order: .reverse)]
        )
        return (try? context.fetch(descriptor)) ?? []
    }

    // MARK: - AirframeMetadata cache

    func airframe(for registration: String) -> AirframeMetadata? {
        let reg = registration.uppercased()
        let descriptor = FetchDescriptor<AirframeMetadata>(
            predicate: #Predicate { $0.registration == reg }
        )
        return (try? context.fetch(descriptor))?.first
    }

    @discardableResult
    func upsertAirframe(
        registration: String,
        firstFlightDate: Date? = nil,
        deliveryDate: Date? = nil
    ) -> AirframeMetadata {
        let reg = registration.uppercased()
        if let existing = airframe(for: reg) {
            if let firstFlightDate { existing.firstFlightDate = firstFlightDate }
            if let deliveryDate { existing.deliveryDate = deliveryDate }
            existing.scrapedAt = Date()
            persist("update airframe metadata")
            return existing
        }
        let m = AirframeMetadata(
            registration: reg,
            firstFlightDate: firstFlightDate,
            deliveryDate: deliveryDate,
            scrapedAt: Date()
        )
        context.insert(m)
        persist("cache airframe metadata")
        return m
    }

    /// How many previously-logged flights have used this same tail
    /// number. Used for the "2nd time on this plane" callout.
    func repeatCount(for registration: String?, before flightDate: Date) -> Int {
        guard let registration, !registration.isEmpty else { return 0 }
        let reg = registration.uppercased()
        let descriptor = FetchDescriptor<LoggedFlight>(
            predicate: #Predicate { f in
                f.registration == reg && f.flightDate < flightDate
            }
        )
        return (try? context.fetch(descriptor))?.count ?? 0
    }

    // MARK: - Distance / duration helpers

    /// Great-circle distance in statute miles between this flight's
    /// dep and arr airports.
    func distanceMiles(for flight: LoggedFlight) -> Int? {
        guard let dep = airportDatabase.airport(byIATA: flight.departureIATA),
              let arr = airportDatabase.airport(byIATA: flight.arrivalIATA)
        else { return nil }
        let depLoc = CLLocation(latitude: dep.coordinate.latitude, longitude: dep.coordinate.longitude)
        let arrLoc = CLLocation(latitude: arr.coordinate.latitude, longitude: arr.coordinate.longitude)
        let meters = depLoc.distance(from: arrLoc)
        return Int(meters / 1609.34)
    }

    /// Duration in minutes — prefers actual times, falls back to
    /// scheduled, returns nil if neither is set.
    func durationMinutes(for flight: LoggedFlight) -> Int? {
        let dep = flight.actualDeparture ?? flight.scheduledDeparture
        let arr = flight.actualArrival ?? flight.scheduledArrival
        guard let dep, let arr, arr > dep else { return nil }
        return Int(arr.timeIntervalSince(dep) / 60)
    }
}
