"""
Eurojackpot-Tipp aus Ziehungs-Historie (Häufigkeiten + Datums-Mix) – nur Unterhaltung.
Echte Gewinnprognose ist mathematisch nicht möglich (Zufall).
"""

from __future__ import annotations

import hashlib
import re
import time
from collections import Counter
from dataclasses import dataclass
from datetime import date, datetime
from math import comb
from pathlib import Path
from urllib.parse import urljoin

import requests

ROOT = Path(__file__).resolve().parent.parent
DATA_FILE = ROOT / "data" / "eurojackpot_history.json"
RESULTS_INDEX = "https://www.euro-jackpot.net/results"
MAIN_MAX = 50
EURO_MAX = 12
MAIN_PICK = 5
EURO_PICK = 2

# Näherung häufiger Zahlen aus Langzeit-Statistik Eurojackpot (2012–2025, öffentl. Auswertungen)
DECADE_MAIN_WEIGHT: dict[int, float] = {
    3: 2.8,
    7: 2.6,
    8: 2.5,
    11: 2.4,
    16: 3.4,
    17: 2.7,
    20: 3.2,
    34: 3.1,
    35: 2.9,
    37: 2.8,
    38: 2.7,
    44: 2.6,
    46: 2.5,
    49: 2.4,
}
DECADE_EURO_WEIGHT: dict[int, float] = {
    3: 3.0,
    4: 2.6,
    5: 3.2,
    7: 2.8,
    8: 2.7,
    10: 2.5,
    11: 2.4,
}


@dataclass
class DrawTip:
    draw_date: date
    main: list[int]
    euro: list[int]
    method: str
    draws_analyzed: int
    disclaimer: str


def parse_draw_date(text: str) -> date | None:
    """5.6.26, 05.06.2026, 5.6.2026 …"""
    t = text.strip().lower().replace("jam", " ")
    m = re.search(r"(\d{1,2})[.\-/](\d{1,2})[.\-/](\d{2,4})", t)
    if not m:
        return None
    d, mo, y = int(m.group(1)), int(m.group(2)), int(m.group(3))
    if y < 100:
        y += 2000
    try:
        return date(y, mo, d)
    except ValueError:
        return None


def hears_eurojackpot(text: str) -> bool:
    h = text.lower().replace("jam", " ")
    if "eurojackpot" not in h and "euro jackpot" not in h:
        if not ("jackpot" in h and "euro" in h):
            if not ("lotto" in h and "zahlen" in h):
                return False
    return any(
        w in h
        for w in (
            "zahl",
            "tipp",
            "sag",
            "berechn",
            "vorhersag",
            "welche",
            "ziehung",
            "wahrschein",
            "chance",
            "gewinnchance",
        )
    )


def _p_class(k_main: int, k_euro: int) -> float:
    """Exakte Wahrscheinlichkeit für genau k_main + k_euro Treffer."""
    p_main = comb(5, k_main) * comb(45, 5 - k_main) / comb(50, 5)
    p_euro = comb(2, k_euro) * comb(10, 2 - k_euro) / comb(12, 2)
    return p_main * p_euro


def probability_report() -> str:
    """Offizielle Gewinnklassen – exakt kombinatorisch (5/50 + 2/12)."""
    total = comb(50, 5) * comb(12, 2)
    rows = [
        ("Jackpot (5+2)", 5, 2),
        ("5+1", 5, 1),
        ("5+0", 5, 0),
        ("4+2", 4, 2),
        ("4+1", 4, 1),
        ("3+2", 3, 2),
        ("4+0", 4, 0),
        ("2+2", 2, 2),
        ("3+1", 3, 1),
        ("3+0", 3, 0),
        ("1+2", 1, 2),
        ("2+1", 2, 1),
    ]
    parts = [
        f"**Exakt:** C(50,5)×C(12,2) = {comb(50,5):,}×{comb(12,2)} = **{total:,}** Tipps.",
        f"**Jackpot:** 1 : {total:,} ({_p_class(5, 2)*100:.7f} %).",
    ]
    p_win = 0.0
    for label, km, ke in rows:
        p = _p_class(km, ke)
        p_win += p
        odds = int(round(1 / p)) if p > 0 else 0
        parts.append(f"{label}: 1:{odds:,} ({p*100:.2f} %)")
    parts.append(
        f"**Mind. ein Gewinn (GK1–12):** ~**{p_win*100:.2f} %** → etwa **1 : {int(round(1/p_win))}**."
    )
    parts.append(
        "**Wichtig:** Jede Kombination ist gleich wahrscheinlich – „häufige Zahlen“ "
        "ändern die Jackpot-Chance pro Schein **nicht**."
    )
    return " ".join(parts)[:720]


def _parse_draw_page(html: str) -> tuple[list[int], list[int]] | None:
    block = re.search(
        r'id="ballsAscending"[^>]*>(.*?)</ul>',
        html,
        re.S | re.I,
    )
    if not block:
        block = re.search(
            r'id="ballsDrawn"[^>]*>(.*?)</ul>',
            html,
            re.S | re.I,
        )
    if not block:
        return None
    inner = block.group(1)
    main = [
        int(m.group(1))
        for m in re.finditer(
            r'<li class="ball"[^>]*>\s*<span>(\d{1,2})</span>',
            inner,
            re.I,
        )
    ]
    euro = [
        int(m.group(1))
        for m in re.finditer(
            r'<li class="euro"[^>]*>\s*<span>(\d{1,2})</span>',
            inner,
            re.I,
        )
    ]
    if len(main) == 5 and len(euro) == 2:
        return main, euro
    return None


def fetch_recent_draws(max_draws: int = 80) -> list[dict]:
    try:
        r = requests.get(RESULTS_INDEX, timeout=20)
        r.raise_for_status()
        html = r.text
    except Exception:
        return []

    links = re.findall(r'href="(/results/\d{2}-\d{2}-\d{4})"', html)
    seen: set[str] = set()
    urls: list[str] = []
    for path in links:
        if path in seen:
            continue
        seen.add(path)
        urls.append(urljoin(RESULTS_INDEX, path))
        if len(urls) >= max_draws:
            break

    draws: list[dict] = []
    for url in urls:
        try:
            p = requests.get(url, timeout=15)
            p.raise_for_status()
            parsed = _parse_draw_page(p.text)
            if not parsed:
                continue
            main, euro = parsed
            dm = re.search(r"/results/(\d{2})-(\d{2})-(\d{4})", url)
            if dm:
                d = date(int(dm.group(3)), int(dm.group(2)), int(dm.group(1)))
            else:
                d = date.today()
            draws.append(
                {"date": d.isoformat(), "main": main, "euro": euro, "url": url}
            )
            time.sleep(0.15)
        except Exception:
            continue
    return draws


def _decade_weights(draws: list[dict]) -> tuple[Counter, Counter]:
    """Gewichtung: Langzeit-Näherung + neuere Ziehungen + Jahrzehnt-Faktor."""
    main_c: Counter = Counter({k: v * 2.0 for k, v in DECADE_MAIN_WEIGHT.items()})
    euro_c: Counter = Counter({k: v * 2.0 for k, v in DECADE_EURO_WEIGHT.items()})
    for i, dr in enumerate(draws):
        age_boost = 1.0 + (len(draws) - i) / max(len(draws), 1)
        y = int(dr["date"][:4])
        decade_boost = 1.15 if y >= 2020 else 1.0
        w = age_boost * decade_boost
        for n in dr["main"]:
            if 1 <= n <= MAIN_MAX:
                main_c[n] += w
        for n in dr["euro"]:
            if 1 <= n <= EURO_MAX:
                euro_c[n] += w
    return main_c, euro_c


def _date_seed_numbers(target: date) -> tuple[list[int], list[int]]:
    """Deterministischer Mix aus Ziehungsdatum (5.6.2026 …)."""
    raw = f"{target.day:02d}{target.month:02d}{target.year}-jam-eurojackpot"
    h = hashlib.sha256(raw.encode()).hexdigest()
    ints = [int(h[i : i + 2], 16) for i in range(0, 40, 2)]

    main: list[int] = []
    pool = list(range(1, MAIN_MAX + 1))
    idx = 0
    while len(main) < MAIN_PICK and pool:
        pick = pool[(ints[idx % len(ints)] + target.day + target.month) % len(pool)]
        if pick not in main:
            main.append(pick)
        pool.remove(pick)
        idx += 1
    main.sort()

    euro: list[int] = []
    epool = list(range(1, EURO_MAX + 1))
    idx = 0
    while len(euro) < EURO_PICK and epool:
        pick = epool[(ints[idx % len(ints)] + target.year % 100) % len(epool)]
        if pick not in euro:
            euro.append(pick)
        epool.remove(pick)
        idx += 1
    euro.sort()
    return main, euro


def _pick_from_freq(
    main_c: Counter,
    euro_c: Counter,
    target: date,
    *,
    hot_ratio: float = 0.6,
) -> tuple[list[int], list[int]]:
    """Mix: häufig gezogene + datums-basierte Zahlen."""
    seed_main, seed_euro = _date_seed_numbers(target)
    main_scores: dict[int, float] = {}
    euro_scores: dict[int, float] = {}

    max_m = max(main_c.values()) if main_c else 1
    max_e = max(euro_c.values()) if euro_c else 1

    for n in range(1, MAIN_MAX + 1):
        freq = main_c.get(n, 0) / max_m
        date_b = 0.35 if n in seed_main else 0.0
        main_scores[n] = freq * hot_ratio + date_b + (n % 7) * 0.02

    for n in range(1, EURO_MAX + 1):
        freq = euro_c.get(n, 0) / max_e
        date_b = 0.35 if n in seed_euro else 0.0
        euro_scores[n] = freq * hot_ratio + date_b

    main = sorted(
        sorted(main_scores, key=main_scores.get, reverse=True)[:7]
    )[:MAIN_PICK]
    # ensure 5 unique – fill from seed if needed
    for n in seed_main:
        if len(main) >= MAIN_PICK:
            break
        if n not in main:
            main.append(n)
    main = sorted(main[:MAIN_PICK])

    euro = sorted(
        sorted(euro_scores, key=euro_scores.get, reverse=True)[:4]
    )[:EURO_PICK]
    for n in seed_euro:
        if len(euro) >= EURO_PICK:
            break
        if n not in euro:
            euro.append(n)
    euro = sorted(euro[:EURO_PICK])
    return main, euro


def calculate_tip(target: date | None = None, *, max_history: int = 80) -> DrawTip:
    target = target or date(2026, 6, 5)
    draws = fetch_recent_draws(max_history)
    if draws:
        try:
            import json

            DATA_FILE.parent.mkdir(parents=True, exist_ok=True)
            DATA_FILE.write_text(
                json.dumps({"updated": time.time(), "draws": draws}, indent=2),
                encoding="utf-8",
            )
        except OSError:
            pass

    if not draws and DATA_FILE.is_file():
        try:
            import json

            draws = json.loads(DATA_FILE.read_text(encoding="utf-8")).get("draws", [])
        except Exception:
            draws = []

    disclaimer = (
        "Keine Garantie – Eurojackpot ist Zufall (1 : 139 Mio.). "
        "Jam mischt Historie + Datums-Berechnung nur als Tipp."
    )

    if not draws:
        main, euro = _date_seed_numbers(target)
        return DrawTip(
            draw_date=target,
            main=main,
            euro=euro,
            method="Datums-Berechnung (keine Historie geladen)",
            draws_analyzed=0,
            disclaimer=disclaimer,
        )

    main_c, euro_c = _decade_weights(draws)
    main, euro = _pick_from_freq(main_c, euro_c, target)
    return DrawTip(
        draw_date=target,
        main=main,
        euro=euro,
        method=(
            f"Langzeit-Statistik (Jahrzehnte-Näherung 2012–2025) + "
            f"{len(draws)} aktuelle Ziehungen + Datum {target.strftime('%d.%m.%Y')}"
        ),
        draws_analyzed=len(draws),
        disclaimer=disclaimer,
    )


def format_tip(tip: DrawTip, owner: str = "David") -> str:
    main_s = " – ".join(f"{n:02d}" for n in tip.main)
    euro_s = " – ".join(f"{n:02d}" for n in tip.euro)
    return (
        f"{owner}, Jam-Tipp für **Eurojackpot am {tip.draw_date.strftime('%d.%m.%Y')}** "
        f"(Freitag-Ziehung):\n\n"
        f"**5 Zahlen:** {main_s}\n"
        f"**2 Eurozahlen:** {euro_s}\n\n"
        f"Methode: {tip.method}.\n"
        f"⚠️ {tip.disclaimer}"
    )


def handle_speech(brain, heard: str) -> str | None:
    if not hears_eurojackpot(heard):
        d = parse_draw_date(heard)
        if not d or "eurojackpot" not in heard.lower() and "jackpot" not in heard.lower():
            return None
    if any(w in heard.lower() for w in ("wahrschein", "chance", "gewinnchance", "rechnung")):
        if not any(w in heard.lower() for w in ("zahl", "tipp", "welche")):
            return probability_report()
    target = parse_draw_date(heard) or date(2026, 6, 6)
    tip = calculate_tip(target)
    owner = getattr(brain.state, "owner_name", "David") if brain else "David"
    if brain:
        brain.state.last_action = "eurojackpot_tip"
        brain.state.remember(
            f"Eurojackpot {target}: {' '.join(map(str, tip.main))} + {' '.join(map(str, tip.euro))}"
        )
        brain.save()
    return format_tip(tip, owner)


if __name__ == "__main__":
    t = calculate_tip(date(2026, 6, 5))
    print(format_tip(t))
