import configparser
import json
import os
import typing
import warnings
from datetime import date, datetime, timedelta, timezone
from decimal import Decimal
from time import time as unixtime

import dateutil
import dateutil.parser
import exchange_calendars
import holidays
import pandas
import pytz
import requests

# from agenda import spacexdata

warnings.simplefilter(action="ignore", category=FutureWarning)


# deadline to file tax return
# credit card expiry dates
# morzine ski lifts
# chalet availablity calendar

# sunrise and sunset
# starlink visible

here = dateutil.tz.tzlocal()
now = datetime.now()
today = now.date()
now_str = now.strftime("%Y-%m-%d_%H:%M")
now_utc = datetime.now(timezone.utc)

next_us_presidential_election = date(2024, 11, 5)
next_uk_general_election = date(2024, 5, 2)

config_filename = os.path.join(os.path.dirname(__file__), "..", "config")

assert os.path.exists(config_filename)

config = configparser.ConfigParser()
config.read(config_filename)

access_key = config.get("exchangerate", "access_key")
data_dir = config.get("data", "dir")


def get_next_timezone_transition(tz_name: str) -> datetime:
    """Datetime of the next time the clocks change."""
    tz = pytz.timezone(tz_name)
    dt = next(t for t in tz._utc_transition_times if t > now)

    return typing.cast(datetime, dt)


def get_next_bank_holiday() -> dict[str, date | str]:
    """Date and name of the next UK bank holiday."""
    url = "https://www.gov.uk/bank-holidays.json"
    filename = os.path.join(data_dir, "bank-holidays.json")
    mtime = os.path.getmtime(filename)
    if (unixtime() - mtime) > 3600:  # one hour
        r = requests.get(url)
        open(filename, "w").write(r.text)

    events = json.load(open(filename))["england-and-wales"]["events"]
    next_holiday = None
    for event in events:
        event_date = datetime.strptime(event["date"], "%Y-%m-%d").date()
        if event_date < today:
            continue
        next_holiday = {"date": event_date, "title": event["title"]}
        break

    assert next_holiday

    return next_holiday


def get_gbpusd() -> Decimal:
    """Get the current value for GBPUSD, with caching."""
    fx_dir = os.path.join(data_dir, "fx")
    existing_data = os.listdir(fx_dir)
    existing = [f for f in existing_data if f.endswith("_GBPUSD.json")]
    if existing:
        recent_filename = max(existing)
        recent = datetime.strptime(recent_filename, "%Y-%m-%d_%H:%M_GBPUSD.json")
        delta = now - recent

    if existing and delta < timedelta(hours=6):
        full = os.path.join(fx_dir, recent_filename)
        data = json.load(open(full), parse_float=Decimal)
        if "quotes" in data and "USDGBP" in data["quotes"]:
            return typing.cast(Decimal, 1 / data["quotes"]["USDGBP"])

    url = "http://api.exchangerate.host/live"
    params = {"currencies": "GBP,USD", "access_key": access_key}

    filename = f"{fx_dir}/{now_str}_GBPUSD.json"
    r = requests.get(url, params=params)
    open(filename, "w").write(r.text)
    data = json.loads(r.text, parse_float=Decimal)

    return typing.cast(Decimal, 1 / data["quotes"]["USDGBP"])


def next_economist() -> date:
    """Next date that the Economist is published."""
    # TODO: handle the Christmas double issue correctly
    return today + timedelta((3 - today.weekday()) % 7)


def timedelta_display(delta: timedelta) -> str:
    """Format timedelta as a human readable string."""
    total_seconds = int(delta.total_seconds())
    days, remainder = divmod(total_seconds, 24 * 60 * 60)
    hours, remainder = divmod(remainder, 60 * 60)
    mins, secs = divmod(remainder, 60)

    return " ".join(
        f"{v:>3} {label}"
        for v, label in ((days, "days"), (hours, "hrs"), (mins, "mins"))
        if v
    )


def stock_markets() -> list[str]:
    """Stock markets open and close times."""
    # The trading calendars code is slow, maybe there is a faster way to do this
    # Or we could cache the result
    now = pandas.Timestamp.now(timezone.utc)
    now_local = pandas.Timestamp.now(here)
    markets = [
        ("XLON", "London"),
        ("XNYS", "US"),
    ]
    reply = []
    for code, label in markets:
        cal = exchange_calendars.get_calendar(code)

        if cal.is_open_on_minute(now_local):
            next_close = cal.next_close(now).tz_convert(here)
            next_close = next_close.replace(minute=round(next_close.minute, -1))
            delta_close = timedelta_display(next_close - now_local)

            prev_open = cal.previous_open(now).tz_convert(here)
            prev_open = prev_open.replace(minute=round(prev_open.minute, -1))
            delta_open = timedelta_display(now_local - prev_open)

            msg = (
                f"{label:>6} market opened {delta_open} ago, "
                + f"closes in {delta_close} ({next_close:%H:%M})"
            )
        else:
            ts = cal.next_open(now)
            ts = ts.replace(minute=round(ts.minute, -1))
            ts = ts.tz_convert(here)
            delta = timedelta_display(ts - now_local)
            msg = f"{label:>6} market opens in {delta}" + (
                f" ({ts:%H:%M})" if (ts - now_local) < timedelta(days=1) else ""
            )

        reply.append(msg)
    return reply


def get_us_holiday() -> dict[str, date | str]:
    """Date and name of next US holiday."""
    hols = holidays.UnitedStates(years=[today.year, today.year + 1])
    next_hol = next(x for x in sorted(hols.items()) if x[0] >= today)

    return {"date": next_hol[0], "title": next_hol[1]}


def get_data() -> dict[str, str | object]:
    """Get data to display on agenda dashboard."""
    reply = {
        "now": now,
        "gbpusd": get_gbpusd(),
        "next_economist": next_economist(),
        "bank_holiday": get_next_bank_holiday(),
        "us_holiday": get_us_holiday(),
        "next_uk_general_election": next_uk_general_election,
        "next_us_presidential_election": next_us_presidential_election,
        # "spacex": spacexdata.get_next_spacex_launch(limit=20),
        "stock_markets": stock_markets(),
        "uk_clock_change": get_next_timezone_transition("Europe/London"),
        "us_clock_change": get_next_timezone_transition("America/New_York"),
    }

    return reply