"""CLI utility functions for display and validation."""

import json
import re
from typing import Any

import plotext as plt
import typer
from click import Context, Parameter
from rich import box
from rich.console import Group
from rich.panel import Panel
from rich.table import Table
from rich.text import Text

from fli.cli.console import console
from fli.cli.enums import DayOfWeek
from fli.core import format_price, format_price_axis_label, google_flights_url
from fli.core.builders import normalize_date
from fli.core.parsers import ParseError
from fli.core.parsers import parse_airlines as core_parse_airlines
from fli.core.parsers import parse_max_stops as core_parse_max_stops
from fli.models import Airline, Airport, MaxStops, TripType


def validate_currency(ctx: Context, param: Parameter, value: str | None) -> str | None:
    """Validate currency code format for typer callbacks."""
    if value is None:
        return None
    normalized = value.upper()
    if not re.fullmatch(r"[A-Z]{3}", normalized):
        raise typer.BadParameter(f"'{value}' is not a valid 3-letter currency code")
    return normalized


def validate_date(ctx: Context, param: Parameter, value: str) -> str | None:
    """Validate date format for typer callbacks."""
    if value is None:
        return None

    try:
        return normalize_date(value)
    except ValueError as e:
        raise typer.BadParameter("Date must be in YYYY-MM-DD format") from e


def validate_time_range(
    ctx: Context, param: Parameter, value: str | None
) -> tuple[int, int] | None:
    """Validate and parse time range in format 'start-end' (24h format) for typer callbacks."""
    if not value:
        return None

    try:
        start, end = map(int, value.split("-"))
        if not (0 <= start <= 23 and 0 <= end <= 23):
            raise ValueError
        return start, end
    except ValueError as e:
        raise typer.BadParameter("Time range must be in format 'start-end' (e.g., 6-20)") from e


def normalize_cli_date(value: str | None) -> str | None:
    """Normalize a CLI date value or raise a parse error."""
    if value is None:
        return None

    try:
        return normalize_date(value)
    except ValueError as e:
        raise ParseError("Date must be in YYYY-MM-DD format") from e


def normalize_cli_time_range(value: str | tuple[int, int] | None) -> tuple[int, int] | None:
    """Normalize a CLI time range or raise a parse error."""
    if value is None:
        return None

    if isinstance(value, tuple):
        start, end = value
    else:
        try:
            start, end = map(int, value.split("-"))
        except ValueError as e:
            raise ParseError("Time range must be in format 'start-end' (e.g., 6-20)") from e

    if not (0 <= start <= 23 and 0 <= end <= 23):
        raise ParseError("Time range must be in format 'start-end' (e.g., 6-20)")

    return start, end


def parse_airlines(airlines: list[str] | None) -> list[Airline] | None:
    """Parse airlines from list of airline codes.

    Delegates to core parser but wraps errors for CLI.
    """
    if not airlines:
        return None

    try:
        return core_parse_airlines(airlines)
    except ParseError as e:
        raise typer.BadParameter(str(e)) from e


def parse_stops(stops: str) -> MaxStops:
    """Convert stops parameter to MaxStops enum.

    Delegates to core parser but wraps errors for CLI.
    """
    try:
        return core_parse_max_stops(stops)
    except ParseError as e:
        raise typer.BadParameter(str(e)) from e


def parse_trip_type(trip_type: str) -> TripType:
    """Convert trip type parameter to TripType enum."""
    match trip_type.upper():
        case "ONEWAY" | "ONE_WAY":
            return TripType.ONE_WAY
        case "ROUND" | "ROUND_TRIP":
            return TripType.ROUND_TRIP
        case _:
            raise typer.BadParameter(f"Invalid trip type: {trip_type}")


def filter_flights_by_time(flights: list, start_hour: int, end_hour: int) -> list:
    """Filter flights by departure time range."""
    return [
        flight
        for flight in flights
        if any(start_hour <= leg.departure_datetime.hour <= end_hour for leg in flight.legs)
    ]


def filter_flights_by_airlines(flights: list, airlines: list[Airline]) -> list:
    """Filter flights by specified airlines."""
    return [flight for flight in flights if any(leg.airline in airlines for leg in flight.legs)]


def filter_dates_by_days(dates: list, days: list[DayOfWeek], trip_type: TripType) -> list:
    """Filter dates by days of the week."""
    if not days:
        return dates

    day_numbers = {
        DayOfWeek.MONDAY: 0,
        DayOfWeek.TUESDAY: 1,
        DayOfWeek.WEDNESDAY: 2,
        DayOfWeek.THURSDAY: 3,
        DayOfWeek.FRIDAY: 4,
        DayOfWeek.SATURDAY: 5,
        DayOfWeek.SUNDAY: 6,
    }

    allowed_days = {day_numbers[day] for day in days}
    return [date_price for date_price in dates if date_price.date[0].weekday() in allowed_days]


def format_airport(airport: Airport) -> str:
    """Format airport code and name (first two words)."""
    name_parts = airport.value.split()[:3]  # Get first three words
    name = " ".join(name_parts)
    return f"{airport.name} ({name})"


def format_duration(minutes: int) -> str:
    """Format duration in minutes to hours and minutes."""
    hours = minutes // 60
    mins = minutes % 60
    return f"{hours}h {mins}m"


def serialize_airport(airport: Airport) -> dict[str, str]:
    """Serialize an airport for machine-readable output."""
    return {"code": airport.name, "name": airport.value}


def serialize_airline(airline: Airline) -> dict[str, str]:
    """Serialize an airline for machine-readable output."""
    return {"code": airline.name.removeprefix("_"), "name": airline.value}


def serialize_flight_leg(leg: Any) -> dict[str, Any]:
    """Serialize a flight leg using Google Flights-style field names."""
    payload: dict[str, Any] = {
        "departure_airport": serialize_airport(leg.departure_airport),
        "arrival_airport": serialize_airport(leg.arrival_airport),
        "departure_time": leg.departure_datetime.isoformat(),
        "arrival_time": leg.arrival_datetime.isoformat(),
        "duration": leg.duration,
        "airline": serialize_airline(leg.airline),
        "flight_number": leg.flight_number,
    }
    if getattr(leg, "aircraft", None):
        payload["aircraft"] = leg.aircraft
    if getattr(leg, "legroom", None):
        payload["legroom"] = leg.legroom
    if getattr(leg, "overnight", False):
        payload["overnight"] = True
    if getattr(leg, "operating_airline", None) is not None:
        payload["operating_airline"] = serialize_airline(leg.operating_airline)
    amenities = getattr(leg, "amenities", None)
    if amenities is not None:
        a = amenities.model_dump(exclude_none=True)
        if a:
            payload["amenities"] = a
    return payload


def _serialize_flight_segment_result(
    flight: Any,
    *,
    include_price: bool = False,
    default_currency: str = "USD",
) -> dict[str, Any]:
    """Serialize a one-direction flight result."""
    payload: dict[str, Any] = {
        "duration": flight.duration,
        "stops": flight.stops,
        "legs": [serialize_flight_leg(leg) for leg in flight.legs],
    }
    if include_price:
        payload["price"] = flight.price
        payload["currency"] = flight.currency or default_currency
    # Optional rich fields surfaced when populated. Emissions are
    # intentionally omitted — the ``--emissions LESS`` filter is still
    # honoured server-side, but CO₂ figures are not rendered in CLI
    # output.
    for src in (
        "self_transfer",
        "mixed_cabin",
    ):
        v = getattr(flight, src, None)
        if v is not None and v != "":
            payload[src] = v
    if getattr(flight, "layovers", None):
        payload["layovers"] = [
            {
                "airport": serialize_airport(lo.airport),
                "duration": lo.duration,
                **({"overnight": True} if lo.overnight else {}),
                **({"change_of_airport": True} if lo.change_of_airport else {}),
            }
            for lo in flight.layovers
        ]
    return payload


def serialize_flight_result(
    flight_data: Any,
    default_currency: str = "USD",
    *,
    booking_url: str | None = None,
) -> dict[str, Any]:
    """Serialize a flight result or round-trip/multi-city tuple for JSON output."""
    if not isinstance(flight_data, tuple):
        out = _serialize_flight_segment_result(
            flight_data, include_price=True, default_currency=default_currency
        )
        if booking_url:
            out["booking_url"] = booking_url
        return out

    segments = list(flight_data)

    if len(segments) == 2:
        # Round-trip: Google Flights returns the full RT price on the outbound leg.
        outbound, return_flight = segments
        out = {
            "price": outbound.price,
            "currency": outbound.currency or default_currency,
            "duration": outbound.duration + return_flight.duration,
            "stops": outbound.stops + return_flight.stops,
            "outbound": _serialize_flight_segment_result(outbound),
            "return": _serialize_flight_segment_result(return_flight),
        }
        if booking_url:
            out["booking_url"] = booking_url
        return out

    # Multi-city (3+ legs): combined price is on the final leg.
    price_segment = segments[-1]
    out = {
        "price": price_segment.price,
        "currency": price_segment.currency or default_currency,
        "duration": sum(s.duration for s in segments),
        "stops": sum(s.stops for s in segments),
        "segments": [_serialize_flight_segment_result(s) for s in segments],
    }
    if booking_url:
        out["booking_url"] = booking_url
    return out


def serialize_date_result(
    date_result: Any,
    trip_type: TripType,
    default_currency: str = "USD",
    *,
    origin: str | None = None,
    destination: str | None = None,
    currency: str | None = None,
    language: str | None = None,
    country: str | None = None,
) -> dict[str, Any]:
    """Serialize a date search result for JSON output.

    When ``origin`` and ``destination`` are provided, a ``booking_url``
    deep-linking to Google Flights for that specific date is included.
    """
    departure_date = date_result.date[0].date().isoformat()
    return_date = None
    if trip_type == TripType.ROUND_TRIP and len(date_result.date) > 1:
        return_date = date_result.date[1].date().isoformat()
    payload = {
        "departure_date": departure_date,
        "return_date": return_date,
        "price": date_result.price,
        "currency": date_result.currency or default_currency,
    }
    if origin and destination:
        payload["booking_url"] = google_flights_url(
            origin,
            destination,
            departure_date,
            return_date,
            currency=currency,
            language=language,
            country=country,
        )
    return payload


def build_json_success_response(
    *,
    search_type: str,
    trip_type: TripType,
    query: dict[str, Any],
    results_key: str,
    results: list[dict[str, Any]],
    booking_url: str | None = None,
) -> dict[str, Any]:
    """Build a JSON success payload for CLI commands."""
    payload: dict[str, Any] = {
        "success": True,
        "data_source": "google_flights",
        "search_type": search_type,
        "trip_type": trip_type.name,
        "query": query,
        "count": len(results),
        results_key: results,
    }
    if booking_url:
        payload["booking_url"] = booking_url
    return payload


def build_json_error_response(
    *,
    search_type: str,
    message: str,
    error_type: str = "validation_error",
    query: dict[str, Any] | None = None,
) -> dict[str, Any]:
    """Build a JSON error payload for CLI commands."""
    payload = {
        "success": False,
        "data_source": "google_flights",
        "search_type": search_type,
        "error": {
            "type": error_type,
            "message": message,
        },
    }
    if query is not None:
        payload["query"] = query
    return payload


def emit_json(payload: dict[str, Any]) -> None:
    """Emit a JSON payload to stdout."""
    typer.echo(json.dumps(payload, indent=2))


def display_flight_results(
    flights: list,
    trip_type: TripType = TripType.ONE_WAY,
    default_currency: str = "USD",
    booking_url: str | None = None,
    booking_urls: list[str | None] | None = None,
):
    """Display flight results in a beautiful format.

    Args:
        flights: List of either FlightResult objects (one-way)
            or tuples of FlightResults (round-trip or multi-city)
        trip_type: The trip type to correctly interpret pricing.
        default_currency: Fallback currency code when Google does not return one.
        booking_url: Optional search-level Google Flights deep link shown as a
            footer so the user can open and browse the full search results.
        booking_urls: Optional per-flight booking deep-link URLs aligned with
            ``flights``.  When provided, a clickable "Book" row is added to
            each flight's info table.

    """
    if not flights:
        console.print(Panel("No flights found matching your criteria", style="red"))
        return

    is_multi_city = trip_type == TripType.MULTI_CITY

    for i, flight_data in enumerate(flights, 1):
        is_multi_leg = isinstance(flight_data, tuple)
        flight_segments = list(flight_data) if is_multi_leg else [flight_data]
        num_legs = len(flight_segments)

        # Create main flight info table
        table = Table(show_header=False, box=box.SIMPLE)
        table.add_column("Label", style="blue")
        table.add_column("Value", style="green")

        # Google Flights returns the full trip price on the outbound leg for round-trips,
        # and on the final leg for multi-city trips.
        price_segment = flight_segments[-1] if is_multi_city else flight_segments[0]
        total_price = price_segment.price
        table.add_row(
            "Total Price", format_price(total_price, price_segment.currency or default_currency)
        )

        total_duration = sum(flight.duration for flight in flight_segments)
        table.add_row("Total Duration", format_duration(total_duration))
        total_stops = sum(flight.stops for flight in flight_segments)
        table.add_row("Total Stops", str(total_stops))

        # Self-transfer / mixed-cabin warnings
        if price_segment.self_transfer:
            table.add_row("Self transfer", "yes (separate tickets)")
        if price_segment.mixed_cabin:
            table.add_row("Mixed cabin", "yes")

        # Per-flight booking deep-link (when available)
        if booking_urls is not None and i - 1 < len(booking_urls):
            burl = booking_urls[i - 1]
            if burl:
                table.add_row("Book", f"[link={burl}]{burl}[/link]")

        # Create segments tables for each direction
        all_segments = []
        for idx, flight in enumerate(flight_segments):
            if num_legs == 1:
                direction = ""
            elif is_multi_city:
                direction = f"Leg {idx + 1}"
            else:
                direction = "Outbound" if idx == 0 else "Return"
            segments = Table(
                title=(f"{direction} Flight Segments" if direction else "Flight Segments"),
                box=box.ROUNDED,
            )
            segments.add_column("Flight", style="cyan", no_wrap=True)
            segments.add_column("From", style="yellow")
            segments.add_column("Depart", style="green", no_wrap=True)
            segments.add_column("To", style="yellow")
            segments.add_column("Arrive", style="green", no_wrap=True)
            segments.add_column("Aircraft", style="magenta")

            for leg_idx, leg in enumerate(flight.legs):
                airline_flight = f"{leg.airline.name.lstrip('_')} {leg.flight_number}"
                arrive_str = leg.arrival_datetime.strftime("%H:%M %d-%b")
                if leg.overnight:
                    arrive_str += " +1"
                segments.add_row(
                    airline_flight,
                    format_airport(leg.departure_airport),
                    leg.departure_datetime.strftime("%H:%M %d-%b"),
                    format_airport(leg.arrival_airport),
                    arrive_str,
                    leg.aircraft or "—",
                )
                # Show layover row between legs when populated.
                if flight.layovers and leg_idx < len(flight.layovers):
                    lo = flight.layovers[leg_idx]
                    where = f"{lo.airport.name} ({lo.city})" if lo.city else lo.airport.name
                    lo_str = f"Layover {format_duration(lo.duration)} at {where}"
                    if lo.overnight:
                        lo_str += " (overnight)"
                    if lo.change_of_airport:
                        lo_str += " (airport change)"
                    segments.add_row("", "", "", "", lo_str, "")
            all_segments.extend([segments, Text("")])

        # Display in a panel
        if is_multi_city:
            title = "Multi-city Flight"
        elif num_legs == 2:
            title = "Round-trip Flight"
        else:
            title = "One-way Flight"
        console.print(
            Panel(
                Group(
                    Text(f"{title} Option {i}", style="bold blue"),
                    Text(""),
                    table,
                    Text(""),
                    *all_segments[:-1],  # Remove the last empty Text
                ),
                title=f"[bold]Option {i} of {len(flights)}[/bold]",
                border_style="blue",
                box=box.ROUNDED,
            )
        )
        console.print()

    if booking_url:
        console.print(
            Panel(
                Text(booking_url, style="cyan"),
                title="[bold]Open on Google Flights[/bold]",
                border_style="cyan",
                box=box.ROUNDED,
            )
        )
        console.print()


def display_date_results(
    dates: list,
    trip_type: TripType,
    default_currency: str = "USD",
    *,
    origin: str | None = None,
    destination: str | None = None,
    currency: str | None = None,
    language: str | None = None,
    country: str | None = None,
):
    """Display date search results with sparkline chart and table."""
    if not dates:
        console.print(Panel("No flights found for these dates", style="red"))
        return

    # Sort dates chronologically for proper trend visualization
    sorted_dates = sorted(dates, key=lambda x: x.date[0])

    # Extract data for chart
    date_labels = [d.date[0].strftime("%m/%d") for d in sorted_dates]
    prices = [d.price for d in sorted_dates]

    # Render sparkline chart
    plt.clear_figure()
    plt.plot(prices, marker="braille")
    plt.title("Price Trend")
    plt.xlabel("Date")
    plt.ylabel(format_price_axis_label(date.currency or default_currency for date in sorted_dates))

    # Set x-axis labels (show subset if too many dates)
    if len(date_labels) <= 10:
        plt.xticks(range(len(date_labels)), date_labels)
    else:
        # Show every nth label to avoid crowding
        step = len(date_labels) // 8
        indices = list(range(0, len(date_labels), step))
        plt.xticks(indices, [date_labels[i] for i in indices])

    plt.theme("pro")
    plt.plotsize(80, 12)
    plt.show()

    console.print()  # Add spacing between chart and table

    # When the route is known, render each departure date as a clickable
    # Google Flights deep link (rich hyperlink — keeps the table narrow while
    # still letting the user jump straight to that date's flights).
    links_enabled = bool(origin and destination)

    def _departure_cell(date_price: Any) -> str:
        label = date_price.date[0].strftime("%Y-%m-%d")
        if not links_enabled:
            return label
        return_date = (
            date_price.date[1].date().isoformat()
            if trip_type == TripType.ROUND_TRIP and len(date_price.date) > 1
            else None
        )
        url = google_flights_url(
            origin,
            destination,
            date_price.date[0].date().isoformat(),
            return_date,
            currency=currency,
            language=language,
            country=country,
        )
        return f"[link={url}]{label}[/link]"

    # Build the table (using original order, not sorted)
    table = Table(
        title="Cheapest Dates to Fly",
        caption="Departure dates link to Google Flights" if links_enabled else None,
        box=box.ROUNDED,
    )
    table.add_column("Departure", style="cyan")
    table.add_column("Day", style="yellow")
    if trip_type == TripType.ROUND_TRIP:
        table.add_column("Return", style="cyan")
        table.add_column("Day", style="yellow")
    table.add_column("Price", style="green")

    for date_price in dates:
        if trip_type == TripType.ONE_WAY:
            table.add_row(
                _departure_cell(date_price),
                date_price.date[0].strftime("%A"),
                format_price(date_price.price, date_price.currency or default_currency),
            )
        else:
            table.add_row(
                _departure_cell(date_price),
                date_price.date[0].strftime("%A"),
                date_price.date[1].strftime("%Y-%m-%d"),
                date_price.date[1].strftime("%A"),
                format_price(date_price.price, date_price.currency or default_currency),
            )

    console.print(table)
    console.print()
