from __future__ import annotations

import asyncio
import json
import sys

import browser_cookie3
import click
from rich.console import Console
from rich.table import Table

from point_me.client import PointMeClient, Site
from point_me.exceptions import AuthError, SearchError, StreamError

_COOKIE_NAME = "__Secure-next-auth.session-token"


def _resolve_token_from_browser(
    browser: str, err: Console
) -> str | None:
    """Try to get an access token from browser cookies.

    Prefers amex.point.me cookies, falls back to www.point.me.
    Returns the access token, or None if resolution failed.
    """
    browser_fn = getattr(browser_cookie3, browser)
    try:
        cj = browser_fn(domain_name=".point.me")
    except Exception as exc:
        err.print(
            f"[yellow]Could not read {browser} cookies: {exc}[/yellow]"
        )
        return None

    amex_cookie = None
    www_cookie = None
    for cookie in cj:
        if cookie.name == _COOKIE_NAME:
            if "amex.point.me" in (cookie.domain or ""):
                amex_cookie = cookie.value
            elif "point.me" in (cookie.domain or ""):
                www_cookie = cookie.value

    if amex_cookie:
        session_cookie = amex_cookie
        domain = "amex.point.me"
        exchange_site = Site.AMEX
    elif www_cookie:
        session_cookie = www_cookie
        domain = "www.point.me"
        exchange_site = Site.WWW
    else:
        err.print(
            "[red]No point.me session cookie found. "
            "Log in to point.me in your browser first.[/red]"
        )
        return None

    err.print(f"[dim]Exchanging session cookie from {domain}...[/dim]")
    try:
        return asyncio.run(
            PointMeClient.exchange_session_cookie(
                session_cookie, exchange_site
            )
        )
    except AuthError as exc:
        err.print(f"[red]Cookie exchange failed: {exc}[/red]")
        return None


def _filter_and_sort(
    routes: list,
    max_stops: int | None = None,
    max_miles: int | None = None,
    program: str | None = None,
    sort_key: str | None = None,
) -> list:
    """Apply filters then sort to route/fare results."""
    result = []
    for route in routes:
        if max_stops is not None and route.stops > max_stops:
            continue
        fares = route.fares
        if max_miles is not None:
            fares = [f for f in fares if f.miles <= max_miles]
        if program is not None:
            fares = [f for f in fares if program.lower() in f.program_name.lower()]
        if not fares:
            continue
        result.append(route.model_copy(update={"fares": fares}))

    if not sort_key:
        return result

    _INF = float("inf")
    pairs = [(route, fare) for route in result for fare in route.fares]

    def _key(pair):
        route, fare = pair
        if sort_key == "miles":
            return fare.miles if fare.miles is not None else _INF
        if sort_key == "stops":
            return route.stops if route.stops is not None else _INF
        if sort_key == "duration":
            val = route.flight_time_minutes
            return val if val is not None else _INF
        return 0

    pairs.sort(key=_key)

    seen: dict = {}
    ordered: list = []
    for route, fare in pairs:
        key = id(route)
        if key not in seen:
            seen[key] = route.model_copy(update={"fares": []})
            ordered.append(seen[key])
        seen[key].fares.append(fare)

    return ordered


def _routes_to_json_rows(routes: list) -> list[dict]:
    rows = []
    for route in routes:
        base = route.model_dump(exclude={"fares"})
        for fare in route.fares:
            rows.append({**base, **fare.model_dump()})
    return rows


@click.group()
def main() -> None:
    pass


@main.command()
@click.argument("origin")
@click.argument("dest")
@click.argument("date")
@click.argument("cabin")
@click.option(
    "--token",
    envvar="POINTME_TOKEN",
    default=None,
    help="API token (or set POINTME_TOKEN)",
)
@click.option(
    "--return-date",
    default=None,
    help="Return date for roundtrip search (YYYY-MM-DD)",
)
@click.option(
    "--json",
    "as_json",
    is_flag=True,
    default=False,
    help="Output raw JSON to stdout",
)
@click.option("--max-stops", type=int, default=None, help="Maximum number of stops")
@click.option("--max-miles", type=int, default=None, help="Maximum miles per fare")
@click.option(
    "--program",
    default=None,
    help="Filter by program name (case-insensitive substring)",
)
@click.option(
    "--sort",
    "sort_key",
    type=click.Choice(["miles", "stops", "duration"]),
    default=None,
    help="Sort results ascending by field",
)
@click.option(
    "--browser",
    type=click.Choice(["firefox", "chrome", "opera", "edge", "chromium"]),
    default="firefox",
    help="Browser to fetch point.me session cookie from "
    "(used when --token is not provided)",
)
def search(
    origin: str,
    dest: str,
    date: str,
    cabin: str,
    token: str | None,
    return_date: str | None,
    as_json: bool,
    max_stops: int | None,
    max_miles: int | None,
    program: str | None,
    sort_key: str | None,
    browser: str,
) -> None:
    out = Console()
    err = Console(stderr=True)

    if not token:
        token = _resolve_token_from_browser(browser, err)

    try:
        client = PointMeClient(token=token)
    except AuthError as exc:
        err.print(f"[red]Auth error: {exc}[/red]")
        sys.exit(1)

    is_roundtrip = return_date is not None

    if is_roundtrip:
        _do_roundtrip_search(
            client, origin, dest, date, return_date, cabin,
            as_json, max_stops, max_miles, program, sort_key, out, err,
        )
    else:
        _do_oneway_search(
            client, origin, dest, date, cabin,
            as_json, max_stops, max_miles, program, sort_key, out, err,
        )


def _do_oneway_search(
    client: PointMeClient,
    origin: str,
    dest: str,
    date: str,
    cabin: str,
    as_json: bool,
    max_stops: int | None,
    max_miles: int | None,
    program: str | None,
    sort_key: str | None,
    out: Console,
    err: Console,
) -> None:
    routes = []
    if as_json:
        try:
            routes = asyncio.run(client.search(origin, dest, date, cabin))
        except (AuthError, SearchError, StreamError) as exc:
            err.print(f"[red]Error: {exc}[/red]")
            sys.exit(1)
    else:
        with err.status("Searching..."):
            try:
                routes = asyncio.run(client.search(origin, dest, date, cabin))
            except (AuthError, SearchError, StreamError) as exc:
                err.print(f"[red]Error: {exc}[/red]")
                sys.exit(1)

    routes = _filter_and_sort(routes, max_stops, max_miles, program, sort_key)

    if not routes:
        err.print("No routes found.")
        sys.exit(0)

    if as_json:
        out.print(json.dumps(_routes_to_json_rows(routes), indent=2))
    else:
        _print_table(out, routes)


def _do_roundtrip_search(
    client: PointMeClient,
    origin: str,
    dest: str,
    date: str,
    return_date: str,
    cabin: str,
    as_json: bool,
    max_stops: int | None,
    max_miles: int | None,
    program: str | None,
    sort_key: str | None,
    out: Console,
    err: Console,
) -> None:
    if as_json:
        try:
            results = asyncio.run(
                client.search_roundtrip(origin, dest, date, return_date, cabin)
            )
        except (AuthError, SearchError, StreamError) as exc:
            err.print(f"[red]Error: {exc}[/red]")
            sys.exit(1)
    else:
        with err.status("Searching..."):
            try:
                results = asyncio.run(
                    client.search_roundtrip(
                        origin, dest, date, return_date, cabin
                    )
                )
            except (AuthError, SearchError, StreamError) as exc:
                err.print(f"[red]Error: {exc}[/red]")
                sys.exit(1)

    outbound = _filter_and_sort(
        results.outbound, max_stops, max_miles, program, sort_key
    )
    inbound = _filter_and_sort(
        results.inbound, max_stops, max_miles, program, sort_key
    )

    if not outbound and not inbound:
        err.print("No routes found.")
        sys.exit(0)

    if as_json:
        out.print(json.dumps({
            "outbound": _routes_to_json_rows(outbound),
            "inbound": _routes_to_json_rows(inbound),
        }, indent=2))
    else:
        if outbound:
            _print_table(out, outbound, title="Outbound Flights")
        else:
            err.print("No outbound routes found.")
        if inbound:
            _print_table(out, inbound, title="Return Flights")
        else:
            err.print("No return routes found.")


def _fmt_duration(minutes: int | None) -> str:
    if minutes is None:
        return "\u2014"
    h, m = divmod(minutes, 60)
    return f"{h}h {m}m"


def _print_table(
    console: Console, routes: list, *, title: str = "Award Flights"
) -> None:
    table = Table(title=title)
    table.add_column("Route", style="cyan", no_wrap=True)
    table.add_column("Program")
    table.add_column("Miles", justify="right")
    table.add_column("Taxes", justify="right")
    table.add_column("Stops", justify="right")
    table.add_column("Duration")

    for route in routes:
        origin = route.connections[0].departure_airport if route.connections else "?"
        dest = route.connections[-1].arrival_airport if route.connections else "?"
        route_str = f"{origin}\u2192{dest}"
        stops_str = str(route.stops)
        duration_str = _fmt_duration(route.flight_time_minutes)

        for fare in route.fares:
            miles_str = f"{fare.miles:,}"
            taxes_str = (
                f"${fare.taxes:.2f}" if fare.taxes is not None else "\u2014"
            )
            table.add_row(
                route_str,
                fare.program_name,
                miles_str,
                taxes_str,
                stops_str,
                duration_str,
            )

    console.print(table)
