import Foundation

actor FlightService {

    static let shared = FlightService()

    // MARK: - Configuration

    private let session: URLSession
    private let baseURL = "https://www.flightconnections.com"

    private static let userAgent =
        "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"

    private var cachedCountries: [Country]?
    private var cachedAirportsByCountry: [String: [BrowseAirport]] = [:]

    init(session: URLSession = .shared) {
        self.session = session
    }

    // MARK: - Public API

    /// Search airports by term (autocomplete).
    func searchAirports(term: String) async throws -> [Airport] {
        let results = try await searchAll(term: term)
        return results.airports
    }

    /// Search airports, countries, and continents by term.
    func searchAll(term: String) async throws -> AutocompleteResults {
        guard let encoded = term.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed),
              let url = URL(string: "\(baseURL)/autocomplete_location.php?lang=en&term=\(encoded)")
        else { throw FlightServiceError.invalidURL }

        let request = makeRequest(url: url)
        let data = try await perform(request)
        let json = try parseJSON(data)

        guard let dict = json as? [String: Any] else {
            return AutocompleteResults(airports: [], countries: [])
        }

        let airportEntries = dict["airports"] as? [[String: Any]] ?? []
        let airports: [Airport] = airportEntries.compactMap { entry in
            guard let value = entry["value"] as? String,
                  let id = entry["id"] as? String
            else { return nil }

            let parts = value.split(separator: " - ", maxSplits: 1)
            guard parts.count == 2 else { return nil }

            let iata = String(parts[0]).trimmingCharacters(in: .whitespaces)
            let name = String(parts[1]).trimmingCharacters(in: .whitespaces)
            return Airport(id: id, iata: iata, name: name)
        }

        let countryEntries = dict["countries"] as? [[String: Any]] ?? []
        let countries: [Country] = countryEntries.compactMap { entry in
            guard let name = entry["name"] as? String,
                  let code = entry["code"] as? String
            else { return nil }
            let slug = name.lowercased().replacingOccurrences(of: " ", with: "-")
            return Country(id: code, name: name, slug: slug)
        }

        return AutocompleteResults(airports: airports, countries: countries)
    }

    /// Fetch all nonstop destinations from a given airport.
    func destinations(for airportId: String) async throws -> [Route] {
        guard let url = URL(string: "\(baseURL)/rt\(airportId).json") else {
            throw FlightServiceError.invalidURL
        }

        let request = makeRequest(url: url)
        let data = try await perform(request)
        let json = try parseJSON(data)

        guard let dict = json as? [String: Any],
              let pts = dict["pts"] as? [Int],
              let crd = dict["crd"] as? [Double],
              let mths = dict["mths"] as? [Int],
              let clss = dict["clss"] as? [Int],
              let lst = dict["lst"] as? [[Any]]
        else { throw FlightServiceError.invalidResponse }

        // pts[0] is origin; lst[i] maps to pts[i+1]
        var routes: [Route] = []
        for (i, item) in lst.enumerated() {
            guard item.count >= 7,
                  let iata = item[0] as? String,
                  let city = item[1] as? String,
                  let countryCode = item[2] as? String,
                  let country = item[3] as? String
            else { continue }

            let durationStr = item[4] as? String ?? "\(item[4])"
            let distanceStr = item[5] as? String ?? "\(item[5])"
            let stopIndicator = item[6] as? String ?? "\(item[6])"

            let duration = Int(durationStr) ?? 0
            let distance = Int(distanceStr) ?? 0

            let ptsIndex = i + 1
            let destId = ptsIndex < pts.count ? String(pts[ptsIndex]) : ""
            let monthBitmask = ptsIndex < mths.count ? mths[ptsIndex] : 0
            let cabinBitmask = i < clss.count ? clss[i] : 0

            let crdIndex = 2 * (i + 1)
            let lat = crdIndex < crd.count ? crd[crdIndex] : 0
            let lng = (crdIndex + 1) < crd.count ? crd[crdIndex + 1] : 0

            let airport = Airport(
                id: destId,
                iata: iata,
                name: city,
                countryCode: countryCode,
                country: country
            )

            routes.append(Route(
                destinationAirport: airport,
                durationMinutes: duration,
                distanceMiles: distance,
                monthsBitmask: monthBitmask,
                cabinClasses: .from(bitmask: cabinBitmask),
                latitude: lat,
                longitude: lng
            ))
        }

        return routes
    }

    /// Fetch airlines operating a specific route.
    func airlines(from depId: String, to desId: String) async throws -> [RouteAirline] {
        guard let url = URL(string: "\(baseURL)/rt\(depId)_\(desId).json") else {
            throw FlightServiceError.invalidURL
        }

        let request = makeRequest(url: url)
        let data = try await perform(request)
        let json = try parseJSON(data)

        guard let dict = json as? [String: Any],
              let dataArray = dict["data"] as? [[String: Any]]
        else { throw FlightServiceError.invalidResponse }

        return dataArray.compactMap { entry in
            guard let routeArray = entry["route"] as? [Any],
                  routeArray.count >= 5,
                  let dist = entry["dist"] as? Int,
                  let dur = entry["dur"] as? Int,
                  let mths = entry["mths"] as? Int
            else { return nil }

            let airlineIdRaw = routeArray[0]
            let airlineId: String
            if let intId = airlineIdRaw as? Int {
                airlineId = String(intId)
            } else if let strId = airlineIdRaw as? String {
                airlineId = strId
            } else {
                return nil
            }

            guard let name = routeArray[2] as? String,
                  let logoFilename = routeArray[3] as? String,
                  let iata = routeArray[4] as? String
            else { return nil }

            let airline = Airline(
                id: airlineId,
                name: name,
                iata: iata,
                logoFilename: logoFilename
            )

            return RouteAirline(
                airline: airline,
                distanceMiles: dist,
                durationMinutes: dur,
                monthsBitmask: mths
            )
        }
    }

    /// Fetch flight schedules for a specific airline on a route.
    func schedules(
        dep depId: String,
        des desId: String,
        airlineId: String,
        airline: Airline
    ) async throws -> [FlightSchedule] {
        guard let url = URL(string: "\(baseURL)/validity.php") else {
            throw FlightServiceError.invalidURL
        }

        let calendar = Calendar.current
        let currentYear = calendar.component(.year, from: Date())
        let nextYear = currentYear + 1

        var request = makeRequest(url: url)
        request.httpMethod = "POST"
        request.setValue(
            "application/x-www-form-urlencoded; charset=UTF-8",
            forHTTPHeaderField: "Content-Type"
        )

        let body = "dep=\(depId)&des=\(desId)&id=\(airlineId)&startDate=\(currentYear)&endDate=\(nextYear)&lang=en"
        request.httpBody = body.data(using: .utf8)

        let data = try await perform(request)
        let json = try parseJSON(data)

        guard let dict = json as? [String: Any],
              let flights = dict["flights"] as? [[String: Any]]
        else { return [] }

        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "yyyy-MM-dd"
        dateFormatter.locale = Locale(identifier: "en_US_POSIX")
        dateFormatter.timeZone = TimeZone(identifier: "UTC")

        let dayKeys: [(String, Int)] = [
            ("su", 1), ("mo", 2), ("tu", 3), ("we", 4),
            ("th", 5), ("fr", 6), ("sa", 7)
        ]

        return flights.compactMap { flight in
            guard let rawFlightNumber = flight["flightnumber"] as? String,
                  let aircraft = flight["aircraft"] as? String,
                  let deptime = flight["deptime"] as? String,
                  let destime = flight["destime"] as? String,
                  let dateFromStr = flight["datefrom"] as? String,
                  let dateToStr = flight["dateto"] as? String,
                  let dateFrom = dateFormatter.date(from: dateFromStr),
                  let dateTo = dateFormatter.date(from: dateToStr)
            else { return nil }

            let flightNumber = rawFlightNumber
                .split(separator: " ")
                .joined(separator: " ")
                .trimmingCharacters(in: .whitespaces)

            let acId: String
            if let idVal = flight["ac_id"] {
                acId = "\(idVal)"
            } else {
                acId = ""
            }

            var days = Set<Int>()
            for (key, weekday) in dayKeys {
                if let val = flight[key] as? String, val == "1" {
                    days.insert(weekday)
                }
            }

            let classesStr = flight["classes"] as? String ?? "0000"
            let cabinClasses = CabinClass.from(classesString: classesStr)

            // Strip seconds from time strings: "16:45:00" -> "16:45"
            let depTimeFormatted = formatTime(deptime)
            let arrTimeFormatted = formatTime(destime)

            return FlightSchedule(
                airline: airline,
                flightNumber: flightNumber,
                aircraft: aircraft,
                aircraftId: acId,
                departureTime: depTimeFormatted,
                arrivalTime: arrTimeFormatted,
                dateFrom: dateFrom,
                dateTo: dateTo,
                daysOfWeek: days,
                cabinClasses: cabinClasses
            )
        }
    }

    /// Fetch all flight schedules for a route across all airlines.
    /// Reports progress via the `onProgress` closure with (completed, total) counts.
    func allSchedules(
        dep depId: String,
        des desId: String,
        onProgress: @Sendable @escaping (Int, Int) -> Void
    ) async throws -> [FlightSchedule] {
        let routeAirlines = try await airlines(from: depId, to: desId)
        let total = routeAirlines.count

        if total == 0 { return [] }

        let completed = Counter()

        let results: [FlightSchedule] = try await withThrowingTaskGroup(of: [FlightSchedule].self) { group in
            var allSchedules: [FlightSchedule] = []
            var launched = 0
            var index = 0

            // Launch initial batch (up to 3)
            while index < routeAirlines.count, launched < 3 {
                let ra = routeAirlines[index]
                group.addTask { [self] in
                    try await self.schedules(
                        dep: depId,
                        des: desId,
                        airlineId: ra.airline.id,
                        airline: ra.airline
                    )
                }
                launched += 1
                index += 1
            }

            // As each completes, launch next
            for try await flights in group {
                allSchedules.append(contentsOf: flights)
                let current = await completed.increment()
                onProgress(current, total)

                if index < routeAirlines.count {
                    let ra = routeAirlines[index]
                    group.addTask { [self] in
                        try await self.schedules(
                            dep: depId,
                            des: desId,
                            airlineId: ra.airline.id,
                            airline: ra.airline
                        )
                    }
                    index += 1
                }
            }

            return allSchedules
        }

        return results.sorted { $0.departureTime < $1.departureTime }
    }

    // MARK: - Browse API

    /// Fetch all countries that have airports.
    func fetchCountries() async throws -> [Country] {
        if let cached = cachedCountries { return cached }

        guard let url = URL(string: "\(baseURL)/airports-by-country") else {
            throw FlightServiceError.invalidURL
        }

        let request = makeRequest(url: url)
        let data = try await perform(request)

        guard let html = String(data: data, encoding: .utf8) else {
            throw FlightServiceError.invalidResponse
        }

        // Pattern: href="/airports-in-{slug}-{code}"
        // Extract country name from slug (convert hyphens to spaces, title case)
        let pattern = try NSRegularExpression(
            pattern: #"href="/airports-in-([a-z-]+)-([a-z]{2})""#
        )
        let matches = pattern.matches(in: html, range: NSRange(html.startIndex..., in: html))

        var seen = Set<String>()
        var countries: [Country] = []

        for match in matches {
            guard let slugRange = Range(match.range(at: 1), in: html),
                  let codeRange = Range(match.range(at: 2), in: html) else { continue }

            let slug = String(html[slugRange])
            let code = String(html[codeRange]).uppercased()

            guard !seen.contains(code) else { continue }
            seen.insert(code)

            let name = slug.split(separator: "-")
                .map { $0.prefix(1).uppercased() + $0.dropFirst() }
                .joined(separator: " ")

            countries.append(Country(id: code, name: name, slug: slug))
        }

        countries.sort { $0.name.localizedCompare($1.name) == .orderedAscending }
        cachedCountries = countries
        return countries
    }

    /// Fetch all airports in a country.
    func fetchAirports(country: Country) async throws -> [BrowseAirport] {
        if let cached = cachedAirportsByCountry[country.id] { return cached }

        guard let url = URL(string: "\(baseURL)/airports-in-\(country.slug)-\(country.id.lowercased())") else {
            throw FlightServiceError.invalidURL
        }

        let request = makeRequest(url: url)
        let data = try await perform(request)

        guard let html = String(data: data, encoding: .utf8) else {
            throw FlightServiceError.invalidResponse
        }

        // Pattern: title="Direct flights from {City} ({IATA})"
        //      or: title="Flights from {City} ({IATA})"
        let pattern = try NSRegularExpression(
            pattern: #"title="(?:Direct )?[Ff]lights from ([^(]+)\(([A-Z]{3})\)""#
        )
        let matches = pattern.matches(in: html, range: NSRange(html.startIndex..., in: html))

        var seen = Set<String>()
        var airports: [BrowseAirport] = []

        for match in matches {
            guard let cityRange = Range(match.range(at: 1), in: html),
                  let iataRange = Range(match.range(at: 2), in: html) else { continue }

            let city = String(html[cityRange]).trimmingCharacters(in: .whitespaces)
            let iata = String(html[iataRange])

            guard !seen.contains(iata) else { continue }
            seen.insert(iata)

            airports.append(BrowseAirport(id: iata, iata: iata, city: city))
        }

        airports.sort { $0.city.localizedCompare($1.city) == .orderedAscending }
        cachedAirportsByCountry[country.id] = airports
        return airports
    }

    // MARK: - Private Helpers

    private func makeRequest(url: URL) -> URLRequest {
        var request = URLRequest(url: url)
        request.setValue(Self.userAgent, forHTTPHeaderField: "User-Agent")
        request.setValue("XMLHttpRequest", forHTTPHeaderField: "X-Requested-With")
        request.setValue("https://www.flightconnections.com/", forHTTPHeaderField: "Referer")
        return request
    }

    private func perform(_ request: URLRequest) async throws -> Data {
        let (data, response) = try await session.data(for: request)
        guard let http = response as? HTTPURLResponse else {
            throw FlightServiceError.invalidResponse
        }
        guard (200...299).contains(http.statusCode) else {
            throw FlightServiceError.httpError(http.statusCode)
        }
        return data
    }

    private func parseJSON(_ data: Data) throws -> Any {
        do {
            return try JSONSerialization.jsonObject(with: data, options: [.fragmentsAllowed])
        } catch {
            throw FlightServiceError.decodingFailed(error)
        }
    }

    private func formatTime(_ raw: String) -> String {
        let components = raw.split(separator: ":")
        if components.count >= 2 {
            return "\(components[0]):\(components[1])"
        }
        return raw
    }
}

// MARK: - Concurrency Helper

private actor Counter {
    private var value = 0

    func increment() -> Int {
        value += 1
        return value
    }
}

// MARK: - Errors

struct AutocompleteResults: Sendable {
    let airports: [Airport]
    let countries: [Country]
}

enum FlightServiceError: LocalizedError {
    case invalidURL
    case invalidResponse
    case httpError(Int)
    case decodingFailed(Error)

    var errorDescription: String? {
        switch self {
        case .invalidURL:
            return "Invalid URL"
        case .invalidResponse:
            return "Invalid response from server"
        case .httpError(let code):
            return "HTTP error \(code)"
        case .decodingFailed(let error):
            return "Failed to decode response: \(error.localizedDescription)"
        }
        }
}
