import heapq
from collections.abc import Sequence
from typing import Optional

from ..core.distance import distance
from ..core.loader import get_airport_by_iata, get_airport_by_icao, get_all_airports
from ..exceptions import RoutingError
from ..models.airport import Airport
from ..utils.constants import DEFAULT_CRUISE_SPEED_KTS, DEFAULT_MAX_LEG_KM
from ..utils.logging import get_logger
from ..utils.units import DistanceUnit, convert_distance

logger = get_logger()


def estimate_flight_time_hours(
    from_airport: Airport,
    to_airport: Airport,
    speed_kts: float = DEFAULT_CRUISE_SPEED_KTS,
    model: str = "haversine",
) -> float:
    dist_nmi = distance(
        from_airport.latitude_deg,
        from_airport.longitude_deg,
        to_airport.latitude_deg,
        to_airport.longitude_deg,
        model=model,
        unit="nmi",
    )

    return dist_nmi / speed_kts


def estimate_flight_time_h_m(
    from_airport: Airport,
    to_airport: Airport,
    speed_kts: float = DEFAULT_CRUISE_SPEED_KTS,
    model: str = "haversine",
) -> tuple[int, int]:
    hours_total = estimate_flight_time_hours(from_airport, to_airport, speed_kts, model)
    hours = int(hours_total)
    minutes = int((hours_total - hours) * 60)

    return (hours, minutes)


def route_distance(
    airports: Sequence[Airport],
    model: str = "haversine",
    unit: DistanceUnit = "km",
) -> float:
    if len(airports) < 2:
        return 0.0

    total_dist_km = 0.0

    for i in range(len(airports) - 1):
        a1 = airports[i]
        a2 = airports[i + 1]

        dist_km = distance(
            a1.latitude_deg,
            a1.longitude_deg,
            a2.latitude_deg,
            a2.longitude_deg,
            model=model,
            unit="km",
        )

        total_dist_km += dist_km

    return convert_distance(total_dist_km, "km", unit)


def route_distance_by_codes(
    codes: Sequence[str],
    code_type: str = "iata",
    model: str = "haversine",
    unit: DistanceUnit = "km",
) -> float:
    airports = []

    for code in codes:
        if code_type == "iata":
            airport = get_airport_by_iata(code)
        elif code_type == "icao":
            airport = get_airport_by_icao(code)
        else:
            airport = get_airport_by_iata(code)
            if airport is None:
                airport = get_airport_by_icao(code)

        if airport is None:
            raise RoutingError(f"Airport not found: {code}")

        airports.append(airport)

    return route_distance(airports, model, unit)


def shortest_path(
    origin_code: str,
    destination_code: str,
    code_type: str = "iata",
    max_leg_km: float = DEFAULT_MAX_LEG_KM,
    allowed_types: Optional[Sequence[str]] = None,
    avoid_countries: Optional[Sequence[str]] = None,
) -> list[Airport]:
    if code_type == "iata":
        origin = get_airport_by_iata(origin_code)
        destination = get_airport_by_iata(destination_code)
    elif code_type == "icao":
        origin = get_airport_by_icao(origin_code)
        destination = get_airport_by_icao(destination_code)
    else:
        origin = get_airport_by_iata(origin_code) or get_airport_by_icao(origin_code)
        destination = get_airport_by_iata(destination_code) or get_airport_by_icao(destination_code)

    if origin is None:
        raise RoutingError(f"Origin airport not found: {origin_code}")
    if destination is None:
        raise RoutingError(f"Destination airport not found: {destination_code}")

    all_airports = get_all_airports()

    if allowed_types:
        all_airports = [a for a in all_airports if a.type in allowed_types]

    if avoid_countries:
        avoid_set = {c.upper() for c in avoid_countries}
        all_airports = [a for a in all_airports if a.iso_country not in avoid_set]

    def airport_key(airport: Airport) -> tuple:
        if airport.id is not None:
            return ("id", airport.id)
        if airport.ident:
            return ("ident", airport.ident)
        if airport.gps_code:
            return ("gps", airport.gps_code)
        if airport.iata_code:
            return ("iata", airport.iata_code)
        return ("coords", airport.latitude_deg, airport.longitude_deg, airport.name)

    airport_map = {airport_key(a): a for a in all_airports}
    origin_key = airport_key(origin)
    destination_key = airport_key(destination)

    if origin_key not in airport_map or destination_key not in airport_map:
        raise RoutingError("Origin or destination excluded by filters")

    def heuristic(a: Airport, b: Airport) -> float:
        return distance(
            a.latitude_deg,
            a.longitude_deg,
            b.latitude_deg,
            b.longitude_deg,
            model="haversine",
            unit="km",
        )

    pq = [(0.0, 0.0, origin_key, [origin])]
    visited = set()

    while pq:
        f_score, g_score, current_key, path = heapq.heappop(pq)

        if current_key in visited:
            continue

        visited.add(current_key)
        current = airport_map[current_key]

        if current_key == destination_key:
            return path

        if len(visited) > 1000:
            logger.warning("Shortest path search exceeded 1000 visited nodes, stopping")
            break

        for neighbor in all_airports:
            neighbor_key = airport_key(neighbor)
            if neighbor_key in visited:
                continue

            leg_dist = heuristic(current, neighbor)

            if leg_dist > max_leg_km:
                continue

            new_g_score = g_score + leg_dist
            new_f_score = new_g_score + heuristic(neighbor, destination)

            heapq.heappush(pq, (new_f_score, new_g_score, neighbor_key, path + [neighbor]))

    raise RoutingError(
        f"No path found from {origin_code} to {destination_code} with given constraints"
    )
