#!/usr/bin/env python3
"""Foresportia tutorial: upcoming matches, markets and verified history.

This script is the canonical source used by the Foresportia documentation
tutorial at /api/docs/tutorials/football-dashboard.html. The page code blocks
and the downloadable file are both generated from this exact file, so they
never diverge. The ``# region`` / ``# endregion`` markers are non-functional
comments used by the documentation generator to slice out each step.

Install:
    python -m pip install --upgrade foresportia matplotlib

API key — Windows PowerShell:
    $env:FORES_API_KEY = "your_key"                          # current session
    [Environment]::SetEnvironmentVariable(
        "FORES_API_KEY", "your_key", "User"
    )                                                        # persistent

API key — macOS / Linux:
    export FORES_API_KEY="your_key"                          # current session
    echo 'export FORES_API_KEY="your_key"' >> ~/.zshrc       # macOS/zsh
    echo 'export FORES_API_KEY="your_key"' >> ~/.bashrc      # Linux/bash

To keep the main path at two API calls, also set the competition:
    $env:FORES_LEAGUE_CODE = "SUE"       # Windows PowerShell
    export FORES_LEAGUE_CODE="SUE"       # macOS / Linux

Without FORES_LEAGUE_CODE, a single-competition Developer key can be detected
with list_leagues(), at the cost of one extra request.

Run:
    python foresportia_tutorial.py
"""

from __future__ import annotations

import os
from datetime import datetime
from pathlib import Path

import matplotlib.pyplot as plt
from foresportia import ForesportiaAPIError, ForesportiaClient, ForesportiaRateLimitError
from foresportia.models import MatchSummary

LEAGUE_CODE = os.getenv("FORES_LEAGUE_CODE")
UPCOMING_DAYS = 14
UPCOMING_LIMIT = 5
SAVE_PNG = False
OUTPUT_DIR = Path("foresportia_charts")

MARKETS = (
    ("btts", "BTTS"),
    ("over_2_5", "Over 2.5"),
    ("under_2_5", "Under 2.5"),
    ("double_chance_1x", "1X"),
    ("double_chance_x2", "X2"),
    ("double_chance_12", "12"),
    ("dnb_home", "DNB 1"),
    ("dnb_away", "DNB 2"),
)


# region choose-league
def choose_league(client: ForesportiaClient) -> str:
    """Use FORES_LEAGUE_CODE or detect the only accessible competition."""
    if LEAGUE_CODE:
        return LEAGUE_CODE.upper()

    leagues = client.list_leagues().data
    if len(leagues) == 1:
        return leagues[0].code
    if not leagues:
        raise RuntimeError("No competition is accessible with this key.")

    codes = ", ".join(league.code for league in leagues)
    raise RuntimeError(
        "Several competitions are accessible. "
        f"Set FORES_LEAGUE_CODE to one of: {codes}"
    )
# endregion choose-league


# region evaluate
def actual_outcome(score: str | None) -> str | None:
    """Turn a final score such as '2-1' into home/draw/away."""
    if not score:
        return None
    try:
        home, away = map(int, score.split("-"))
    except ValueError:
        return None
    return "home" if home > away else "away" if away > home else "draw"


def pick_is_correct(match: MatchSummary) -> bool | None:
    """Compare the published 1X2 pick with the verified final score."""
    actual = actual_outcome(match.result_score) if match.is_final else None
    predicted = match.predicted_outcome
    return None if actual is None or predicted is None else actual == predicted
# endregion evaluate


def confidence_text(match: MatchSummary) -> str:
    """Show the badge when present, otherwise the confidence score."""
    confidence = match.confidence or {}
    if confidence.get("label"):
        return str(confidence["label"])
    if confidence.get("badge"):
        return str(confidence["badge"])
    score = confidence.get("score")
    return f"confidence {score:.2f}" if isinstance(score, (int, float)) else "not provided"


def date_label(match: MatchSummary) -> str:
    kickoff = match.kickoff_local or match.kickoff
    try:
        return datetime.fromisoformat(kickoff.replace("Z", "+00:00")).strftime("%d/%m")
    except (AttributeError, ValueError):
        return "--/--"


# region plot-upcoming
def plot_upcoming(league_name: str, matches: list[MatchSummary]) -> plt.Figure:
    """1X2 probabilities, main pick and confidence."""
    figure, axis = plt.subplots(figsize=(13, max(4.8, len(matches) * 0.9)))
    if not matches:
        axis.axis("off")
        axis.text(0.5, 0.5, "No upcoming published match.", ha="center", va="center")
        return figure

    y = list(range(len(matches)))
    home = [m.probabilities["home"] * 100 for m in matches]
    draw = [m.probabilities["draw"] * 100 for m in matches]
    away = [m.probabilities["away"] * 100 for m in matches]

    axis.barh(y, home, label="1 · Home")
    axis.barh(y, draw, left=home, label="X · Draw")
    axis.barh(y, away, left=[h + d for h, d in zip(home, draw)], label="2 · Away")
    axis.set_yticks(y, [f"{date_label(m)} · {m.home_team} – {m.away_team}" for m in matches])
    axis.set_xlim(0, 140)
    axis.set_xticks((0, 20, 40, 60, 80, 100))
    axis.set_xlabel("1X2 probability (%)")
    axis.set_title(f"{league_name} — upcoming matches")
    axis.grid(axis="x", alpha=0.25)
    axis.invert_yaxis()
    axis.legend(ncol=3, loc="upper center", bbox_to_anchor=(0.42, -0.12))

    for index, match in enumerate(matches):
        probability = (match.pick or {}).get("probability")
        probability_text = f"{probability:.0%}" if isinstance(probability, (int, float)) else ""
        axis.text(
            102,
            index,
            f"★ {match.predicted_outcome or 'no pick'} {probability_text} · {confidence_text(match)}",
            va="center",
            fontsize=9,
        )

    figure.tight_layout()
    return figure
# endregion plot-upcoming


# region plot-markets
def plot_markets(league_name: str, matches: list[MatchSummary]) -> plt.Figure:
    """Matrix of the markets actually present in the response."""
    available = {key for match in matches for key in (match.markets or {})}
    columns = [(key, label) for key, label in MARKETS if key in available]
    figure, axis = plt.subplots(
        figsize=(max(10, len(columns) * 1.4), max(4.8, len(matches) * 0.9))
    )

    if not matches or not columns:
        axis.axis("off")
        axis.text(0.5, 0.5, "No additional market available.", ha="center", va="center")
        return figure

    matrix = [[(m.markets or {}).get(key, float("nan")) for key, _ in columns] for m in matches]
    image = axis.imshow(matrix, aspect="auto", vmin=0, vmax=1)
    axis.set_xticks(range(len(columns)), [label for _, label in columns])
    axis.set_yticks(range(len(matches)), [f"{m.home_team} – {m.away_team}" for m in matches])
    axis.set_title(f"{league_name} — available markets")

    for row_index, row in enumerate(matrix):
        for column_index, probability in enumerate(row):
            if probability == probability:  # NaN-safe
                axis.text(column_index, row_index, f"{probability:.0%}", ha="center", va="center")

    figure.colorbar(image, ax=axis, label="Probability")
    figure.tight_layout()
    return figure
# endregion plot-markets


# region plot-history
def plot_history(league_name: str, history: list[MatchSummary]) -> plt.Figure:
    """Cumulative success curve with result and confidence."""
    decided = [match for match in history if pick_is_correct(match) is not None]
    figure, axis = plt.subplots(figsize=(12, 6))

    if not decided:
        axis.axis("off")
        axis.text(0.5, 0.5, "No verified result in the history window.", ha="center", va="center")
        return figure

    successes = 0
    cumulative = []
    for index, match in enumerate(decided, start=1):
        successes += int(pick_is_correct(match) is True)
        cumulative.append(successes / index)

    x = list(range(1, len(decided) + 1))
    axis.plot(x, cumulative, marker="o")
    axis.axhline(0.5, linestyle="--", linewidth=1)
    axis.set_ylim(0, 1)
    axis.set_xlabel("Historical match")
    axis.set_ylabel("Cumulative success rate")
    axis.set_title(
        f"{league_name} — history: {successes}/{len(decided)} correct "
        f"({cumulative[-1]:.1%})"
    )
    axis.grid(alpha=0.25)

    for index, match in enumerate(decided):
        verdict = "✓" if pick_is_correct(match) else "✗"
        axis.annotate(
            f"{verdict} {confidence_text(match)}\n{match.result_score}",
            (x[index], cumulative[index]),
            xytext=(0, 10),
            textcoords="offset points",
            ha="center",
            fontsize=8,
        )

    if len(decided) < 20:
        axis.text(0.01, 0.03, "Small sample: descriptive reading only.", transform=axis.transAxes)

    figure.tight_layout()
    return figure
# endregion plot-history


def main() -> int:
    try:
        with ForesportiaClient.from_env(timeout=20.0, max_retries=0) as client:
            league_code = choose_league(client)
            # region upcoming-call
            upcoming_response = client.list_league_matches(
                league_code,
                include="upcoming",
                days=UPCOMING_DAYS,
                limit=UPCOMING_LIMIT,
            )
            # endregion upcoming-call
            # region history-call
            history_response = client.list_league_history(league_code)
            # endregion history-call

        upcoming = upcoming_response.data[:UPCOMING_LIMIT]
        history = history_response.data
        reference = upcoming[0] if upcoming else history[0] if history else None
        league_name = f"{reference.league.name} ({reference.league.code})" if reference else league_code

        decided = [match for match in history if pick_is_correct(match) is not None]
        successes = sum(pick_is_correct(match) is True for match in decided)
        print(f"\n{league_name}")
        print(f"Upcoming matches: {len(upcoming)}")
        print(f"History received: {len(history)}")
        if decided:
            print(f"Success: {successes}/{len(decided)} ({successes / len(decided):.1%})")
        print("History window:", history_response.payload.get("date_range"))
        print("Entitlement:", history_response.history_entitlement_days, "day(s)")

        figures = [
            ("01_upcoming_1x2.png", plot_upcoming(league_name, upcoming)),
            ("02_markets.png", plot_markets(league_name, upcoming)),
            ("03_history.png", plot_history(league_name, history)),
        ]

        if SAVE_PNG:
            OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
            for filename, figure in figures:
                path = OUTPUT_DIR / filename
                figure.savefig(path, dpi=170, bbox_inches="tight", pad_inches=0.2)
                print("Saved:", path.resolve())

        plt.show()
        return 0

    except ForesportiaRateLimitError as error:
        print("Temporary quota reached. Retry-After:", error.retry_after)
        return 2
    except ForesportiaAPIError as error:
        print("API error:", error.status_code, error.error_code, error.endpoint)
        return 1
    except RuntimeError as error:
        print("Error:", error)
        return 1


if __name__ == "__main__":
    raise SystemExit(main())
