#!/usr/bin/python3

"""Web page to show upcoming events."""

import decimal
import inspect
import operator
import os.path
import sys
import time
import traceback
from collections import defaultdict
from datetime import date, datetime, timedelta

import flask
import UniAuth.auth
import werkzeug
import werkzeug.debug.tbtools
import yaml

import agenda.data
import agenda.error_mail
import agenda.fx
import agenda.holidays
import agenda.stats
import agenda.thespacedevs
import agenda.trip
import agenda.utils
from agenda import calendar, format_list_with_ampersand, travel, uk_tz
from agenda.types import StrDict, Trip

app = flask.Flask(__name__)
app.debug = False
app.config.from_object("config.default")

agenda.error_mail.setup_error_mail(app)


@app.before_request
def handle_auth() -> None:
    """Handle authentication and set global user."""
    flask.g.user = UniAuth.auth.get_current_user()


@app.errorhandler(werkzeug.exceptions.InternalServerError)
def exception_handler(e: werkzeug.exceptions.InternalServerError) -> tuple[str, int]:
    """Handle exception."""
    exec_type, exc_value, current_traceback = sys.exc_info()
    assert exc_value
    tb = werkzeug.debug.tbtools.DebugTraceback(exc_value)

    summary = tb.render_traceback_html(include_title=False)
    exc_lines = "".join(tb._te.format_exception_only())

    last_frame = list(traceback.walk_tb(current_traceback))[-1][0]
    last_frame_args = inspect.getargs(last_frame.f_code)

    return (
        flask.render_template(
            "show_error.html",
            plaintext=tb.render_traceback_text(),
            exception=exc_lines,
            exception_type=tb._te.exc_type.__name__,
            summary=summary,
            last_frame=last_frame,
            last_frame_args=last_frame_args,
        ),
        500,
    )


def get_current_trip(today: date) -> Trip | None:
    """Get current trip."""
    trip_list = get_trip_list(route_distances=None)

    current = [
        item
        for item in trip_list
        if item.start <= today and (item.end or item.start) >= today
    ]
    assert len(current) < 2
    return current[0] if current else None


@app.route("/")
async def index() -> str:
    """Index page."""
    t0 = time.time()
    now = datetime.now()
    data = await agenda.data.get_data(now, app.config)

    events = data.pop("events")
    today = now.date()

    markets_arg = flask.request.args.get("markets")
    if markets_arg == "hide":
        events = [e for e in events if e.name != "market"]
    if markets_arg != "show":
        agenda.data.hide_markets_while_away(events, data["accommodation_events"])

    return flask.render_template(
        "event_list.html",
        today=today,
        events=events,
        get_country=agenda.get_country,
        current_trip=get_current_trip(today),
        fullcalendar_events=calendar.build_events(events),
        start_event_list=date.today() - timedelta(days=1),
        end_event_list=date.today() + timedelta(days=365 * 2),
        render_time=(time.time() - t0),
        **data,
    )


@app.route("/calendar")
async def calendar_page() -> str:
    """Index page."""
    now = datetime.now()
    data = await agenda.data.get_data(now, app.config)

    events = data.pop("events")

    markets_arg = flask.request.args.get("markets")
    if markets_arg == "hide":
        events = [e for e in events if e.name != "market"]
    if markets_arg != "show":
        agenda.data.hide_markets_while_away(events, data["accommodation_events"])

    return flask.render_template(
        "calendar.html",
        today=now.date(),
        events=events,
        fullcalendar_events=calendar.build_events(events),
        **data,
    )


@app.route("/recent")
async def recent() -> str:
    """Index page."""
    t0 = time.time()
    now = datetime.now()
    data = await agenda.data.get_data(now, app.config)

    events = data.pop("events")

    markets_arg = flask.request.args.get("markets")
    if markets_arg == "hide":
        events = [e for e in events if e.name != "market"]
    if markets_arg != "show":
        agenda.data.hide_markets_while_away(events, data["accommodation_events"])

    return flask.render_template(
        "event_list.html",
        today=now.date(),
        events=events,
        fullcalendar_events=calendar.build_events(events),
        start_event_list=date.today() - timedelta(days=14),
        end_event_list=date.today(),
        render_time=(time.time() - t0),
        **data,
    )


@app.route("/launches")
def launch_list() -> str:
    """Web page showing List of space launches."""
    now = datetime.now()
    data_dir = app.config["DATA_DIR"]
    rocket_dir = os.path.join(data_dir, "thespacedevs")
    launches = agenda.thespacedevs.get_launches(rocket_dir, limit=100)
    assert launches

    mission_type_filter = flask.request.args.get("type")
    rocket_filter = flask.request.args.get("rocket")
    orbit_filter = flask.request.args.get("orbit")

    mission_types = {
        launch["mission"]["type"] for launch in launches if launch["mission"]
    }

    orbits = {
        (launch["orbit"]["name"], launch["orbit"]["abbrev"])
        for launch in launches
        if launch.get("orbit")
    }
    rockets = {launch["rocket"]["full_name"] for launch in launches}

    launches = [
        launch
        for launch in launches
        if (
            not mission_type_filter
            or (launch["mission"] and launch["mission"]["type"] == mission_type_filter)
        )
        and (not rocket_filter or launch["rocket"]["full_name"] == rocket_filter)
        and (
            not orbit_filter
            or (launch.get("orbit") and launch["orbit"]["abbrev"] == orbit_filter)
        )
    ]

    return flask.render_template(
        "launches.html",
        launches=launches,
        rockets=rockets,
        now=now,
        get_country=agenda.get_country,
        mission_types=mission_types,
        orbits=orbits,
    )


@app.route("/gaps")
async def gaps_page() -> str:
    """List of available gaps."""
    now = datetime.now()
    trip_list = agenda.trip.build_trip_list()
    busy_events = agenda.busy.get_busy_events(now.date(), app.config, trip_list)
    gaps = agenda.busy.find_gaps(busy_events)
    return flask.render_template("gaps.html", today=now.date(), gaps=gaps)


@app.route("/weekends")
async def weekends() -> str:
    """List of available gaps."""
    now = datetime.now()
    trip_list = agenda.trip.build_trip_list()
    busy_events = agenda.busy.get_busy_events(now.date(), app.config, trip_list)
    weekends = agenda.busy.weekends(busy_events)
    return flask.render_template("weekends.html", today=now.date(), items=weekends)


@app.route("/travel")
def travel_list() -> str:
    """Page showing a list of upcoming travel."""
    data_dir = app.config["PERSONAL_DATA"]
    flights = agenda.trip.load_flight_bookings(data_dir)
    trains = [
        item
        for item in travel.parse_yaml("trains", data_dir)
        if isinstance(item["depart"], datetime)
    ]

    route_distances = agenda.travel.load_route_distances(app.config["DATA_DIR"])

    for train in trains:
        for leg in train["legs"]:
            agenda.travel.add_leg_route_distance(leg, route_distances)

        if all("distance" in leg for leg in train["legs"]):
            train["distance"] = sum(leg["distance"] for leg in train["legs"])

    return flask.render_template(
        "travel.html",
        flights=flights,
        trains=trains,
        fx_rate=agenda.fx.get_rates(app.config),
    )


def build_conference_list() -> list[StrDict]:
    """Build conference list."""
    data_dir = app.config["PERSONAL_DATA"]
    filepath = os.path.join(data_dir, "conferences.yaml")
    items: list[StrDict] = yaml.safe_load(open(filepath))
    conference_trip_lookup = {}

    for trip in agenda.trip.build_trip_list():
        for trip_conf in trip.conferences:
            key = (trip_conf["start"], trip_conf["name"])
            conference_trip_lookup[key] = trip

    for conf in items:
        conf["start_date"] = agenda.utils.as_date(conf["start"])
        conf["end_date"] = agenda.utils.as_date(conf["end"])

        price = conf.get("price")
        if price:
            conf["price"] = decimal.Decimal(price)

        key = (conf["start"], conf["name"])
        if this_trip := conference_trip_lookup.get(key):
            conf["linked_trip"] = this_trip

    items.sort(key=operator.itemgetter("start_date"))
    return items


@app.route("/conference")
def conference_list() -> str:
    """Page showing a list of conferences."""
    today = date.today()
    items = build_conference_list()

    current = [
        conf
        for conf in items
        if conf["start_date"] <= today and conf["end_date"] >= today
    ]
    future = [conf for conf in items if conf["start_date"] > today]

    return flask.render_template(
        "conference_list.html",
        current=current,
        future=future,
        today=today,
        get_country=agenda.get_country,
        fx_rate=agenda.fx.get_rates(app.config),
    )


@app.route("/conference/past")
def past_conference_list() -> str:
    """Page showing a list of conferences."""
    today = date.today()
    return flask.render_template(
        "conference_list.html",
        past=[conf for conf in build_conference_list() if conf["end_date"] < today],
        today=today,
        get_country=agenda.get_country,
        fx_rate=agenda.fx.get_rates(app.config),
    )


@app.route("/accommodation")
def accommodation_list() -> str:
    """Page showing a list of past, present and future accommodation."""
    data_dir = app.config["PERSONAL_DATA"]
    items = travel.parse_yaml("accommodation", data_dir)

    stays_in_2024 = [item for item in items if item["from"].year == 2024]
    total_nights_2024 = sum(
        (stay["to"].date() - stay["from"].date()).days for stay in stays_in_2024
    )

    nights_abroad_2024 = sum(
        (stay["to"].date() - stay["from"].date()).days
        for stay in stays_in_2024
        if stay["country"] != "gb"
    )

    trip_lookup = {}

    for trip in agenda.trip.build_trip_list():
        for trip_stay in trip.accommodation:
            key = (trip_stay["from"], trip_stay["name"])
            trip_lookup[key] = trip

    for item in items:
        key = (item["from"], item["name"])
        if this_trip := trip_lookup.get(key):
            item["linked_trip"] = this_trip

    now = uk_tz.localize(datetime.now())

    past = [conf for conf in items if conf["to"] < now]
    current = [conf for conf in items if conf["from"] <= now and conf["to"] >= now]
    future = [conf for conf in items if conf["from"] > now]

    return flask.render_template(
        "accommodation.html",
        past=past,
        current=current,
        future=future,
        total_nights_2024=total_nights_2024,
        nights_abroad_2024=nights_abroad_2024,
        get_country=agenda.get_country,
        fx_rate=agenda.fx.get_rates(app.config),
    )


def get_trip_list(
    route_distances: agenda.travel.RouteDistances | None = None,
) -> list[Trip]:
    """Get list of trips respecting current authentication status."""
    return [
        trip
        for trip in agenda.trip.build_trip_list(route_distances=route_distances)
        if flask.g.user.is_authenticated or not trip.private
    ]


@app.route("/trip")
def trip_list() -> werkzeug.Response:
    """Trip list to redirect to future trip list."""
    return flask.redirect(flask.url_for("trip_future_list"))


def calc_total_distance(trips: list[Trip]) -> float:
    """Total distance for trips."""
    total = 0.0
    for item in trips:
        dist = item.total_distance()
        if dist:
            total += dist

    return total


def sum_distances_by_transport_type(trips: list[Trip]) -> list[tuple[str, float]]:
    """Sum distances by transport type."""
    distances_by_transport_type: defaultdict[str, float] = defaultdict(float)
    for trip in trips:
        for transport_type, dist in trip.distances_by_transport_type():
            distances_by_transport_type[transport_type] += dist

    return list(distances_by_transport_type.items())


@app.route("/trip/past")
def trip_past_list() -> str:
    """Page showing a list of past trips."""
    route_distances = agenda.travel.load_route_distances(app.config["DATA_DIR"])
    trip_list = get_trip_list(route_distances)
    today = date.today()

    past = [item for item in trip_list if (item.end or item.start) < today]

    coordinates, routes = agenda.trip.get_coordinates_and_routes(past)

    return flask.render_template(
        "trip/list.html",
        heading="Past trips",
        trips=reversed(past),
        coordinates=coordinates,
        routes=routes,
        today=today,
        get_country=agenda.get_country,
        format_list_with_ampersand=format_list_with_ampersand,
        fx_rate=agenda.fx.get_rates(app.config),
        total_distance=calc_total_distance(past),
        distances_by_transport_type=sum_distances_by_transport_type(past),
    )


@app.route("/trip/future")
def trip_future_list() -> str:
    """Page showing a list of future trips."""
    route_distances = agenda.travel.load_route_distances(app.config["DATA_DIR"])
    trip_list = get_trip_list(route_distances)
    today = date.today()

    current = [
        item
        for item in trip_list
        if item.start <= today and (item.end or item.start) >= today
    ]

    future = [item for item in trip_list if item.start > today]

    coordinates, routes = agenda.trip.get_coordinates_and_routes(current + future)

    return flask.render_template(
        "trip/list.html",
        heading="Future trips",
        trips=current + future,
        coordinates=coordinates,
        routes=routes,
        today=today,
        get_country=agenda.get_country,
        format_list_with_ampersand=format_list_with_ampersand,
        fx_rate=agenda.fx.get_rates(app.config),
        total_distance=calc_total_distance(current + future),
        distances_by_transport_type=sum_distances_by_transport_type(current + future),
    )


@app.route("/trip/text")
def trip_list_text() -> str:
    """Page showing a list of trips."""
    trip_list = get_trip_list()
    today = date.today()
    future = [item for item in trip_list if item.start > today]

    return flask.render_template(
        "trip_list_text.html",
        future=future,
        today=today,
        get_country=agenda.get_country,
        format_list_with_ampersand=format_list_with_ampersand,
    )


def get_prev_current_and_next_trip(
    start: str, trip_list: list[Trip]
) -> tuple[Trip | None, Trip | None, Trip | None]:
    """Get previous trip, this trip and next trip."""
    trip_iter = iter(trip_list)
    prev_trip = None
    current_trip = None
    for trip in trip_iter:
        if trip.start.isoformat() == start:
            current_trip = trip
            break
        prev_trip = trip
    next_trip = next(trip_iter, None)

    return (prev_trip, current_trip, next_trip)


@app.route("/trip/<start>")
def trip_page(start: str) -> str:
    """Individual trip page."""
    route_distances = agenda.travel.load_route_distances(app.config["DATA_DIR"])
    trip_list = get_trip_list(route_distances)

    prev_trip, trip, next_trip = get_prev_current_and_next_trip(start, trip_list)
    if not trip:
        flask.abort(404)

    coordinates = agenda.trip.collect_trip_coordinates(trip)
    routes = agenda.trip.get_trip_routes(trip, app.config["PERSONAL_DATA"])

    agenda.trip.add_coordinates_for_unbooked_flights(routes, coordinates)

    for route in routes:
        if "geojson_filename" in route:
            route["geojson"] = agenda.trip.read_geojson(
                app.config["PERSONAL_DATA"], route.pop("geojson_filename")
            )

    return flask.render_template(
        "trip_page.html",
        trip=trip,
        prev_trip=prev_trip,
        next_trip=next_trip,
        today=date.today(),
        coordinates=coordinates,
        routes=routes,
        get_country=agenda.get_country,
        format_list_with_ampersand=format_list_with_ampersand,
        holidays=agenda.holidays.get_trip_holidays(trip),
        human_readable_delta=agenda.utils.human_readable_delta,
    )


@app.route("/holidays")
def holiday_list() -> str:
    """List of holidays."""
    today = date.today()
    data_dir = app.config["DATA_DIR"]
    next_year = today + timedelta(days=1 * 365)
    items = agenda.holidays.get_all(today - timedelta(days=2), next_year, data_dir)

    items.sort(key=lambda item: (item.date, item.country))

    return flask.render_template(
        "holiday_list.html", items=items, get_country=agenda.get_country, today=today
    )


@app.route("/birthdays")
def birthday_list() -> str:
    """List of birthdays."""
    today = date.today()
    if not flask.g.user.is_authenticated:
        flask.abort(401)
    data_dir = app.config["PERSONAL_DATA"]
    entities_file = os.path.join(data_dir, "entities.yaml")
    items = agenda.birthday.get_birthdays(today - timedelta(days=2), entities_file)
    items.sort(key=lambda item: item.date)
    return flask.render_template("birthday_list.html", items=items, today=today)


@app.route("/trip/stats")
def trip_stats() -> str:
    """Travel stats: distance and price by year and travel type."""
    route_distances = agenda.travel.load_route_distances(app.config["DATA_DIR"])
    trip_list = get_trip_list(route_distances)

    conferences = sum(len(item.conferences) for item in trip_list)

    yearly_stats = agenda.stats.calculate_yearly_stats(trip_list)

    return flask.render_template(
        "trip/stats.html",
        count=len(trip_list),
        total_distance=calc_total_distance(trip_list),
        distances_by_transport_type=sum_distances_by_transport_type(trip_list),
        yearly_stats=yearly_stats,
        conferences=conferences,
    )


@app.route("/callback")
def auth_callback() -> tuple[str, int] | werkzeug.Response:
    """Process the authentication callback."""
    return UniAuth.auth.auth_callback()


@app.route("/login")
def login() -> werkzeug.Response:
    """Login."""
    next_url = flask.request.args["next"]
    return UniAuth.auth.redirect_to_login(next_url)


@app.route("/logout")
def logout() -> werkzeug.Response:
    """Logout."""
    return UniAuth.auth.redirect_to_logout(flask.request.args["next"])


if __name__ == "__main__":
    app.run(host="0.0.0.0")