"""
Service layer for TapIN MMA.

Provides:
- FighterService: Octagon API fighter data
- OddsService: The Odds API odds data
- EventsService: Tapology (RapidAPI) events + geocoding
- EmailService: subscriber list management + Mailjet sending
- maybe_schedule_daily_mailjob: background digest email scheduler
"""

import os
import json
from datetime import datetime, timedelta, timezone
from typing import Dict, Any, List, Optional, Tuple

import pandas as pd
import requests
import streamlit as st

try:
    from apscheduler.schedulers.background import BackgroundScheduler  # type: ignore
except Exception:
    BackgroundScheduler = None

# ---------------------------------------------------------
# Time helpers
# ---------------------------------------------------------
def now_utc() -> datetime:
    return datetime.now(timezone.utc)


# ---------------------------------------------------------
# Fighter data (Octagon API)
# ---------------------------------------------------------
@st.cache_data(ttl=3600)
def _load_octagon_fighters_cached() -> List[Dict[str, Any]]:
    try:
        res = requests.get("https://api.octagon-api.com/fighters", timeout=12)
        res.raise_for_status()
        data = res.json()
        return list(data.values()) if isinstance(data, dict) else data
    except Exception:
        return []


class FighterService:
    def get_fighters(self) -> List[Dict[str, Any]]:
        return _load_octagon_fighters_cached()


# ---------------------------------------------------------
# Odds helpers (The Odds API)
# ---------------------------------------------------------
def american_to_implied_prob(odds: Optional[int]) -> Optional[float]:
    if odds is None:
        return None
    try:
        o = int(odds)
        if o > 0:
            return 100 / (o + 100)
        elif o < 0:
            return (-o) / ((-o) + 100)
        return None
    except Exception:
        return None


@st.cache_data(ttl=900, show_spinner=False)
def _fetch_odds_from_api_cached() -> Tuple[pd.DataFrame, Optional[str]]:
    try:
        api_key = st.secrets["api"]["odds_api_key"]
    except Exception:
        api_key = None

    if not api_key:
        empty_df = pd.DataFrame(
            columns=["matchup", "book", "team", "price", "implied", "last_update_iso"]
        )
        return empty_df, None

    url = "https://api.the-odds-api.com/v4/sports/mma_mixed_martial_arts/odds"
    params = {
        "regions": "us,uk,eu",
        "markets": "h2h",
        "oddsFormat": "american",
        "dateFormat": "iso",
        "apiKey": api_key,
    }

    try:
        r = requests.get(url, params=params, timeout=10)
        r.raise_for_status()
        data = r.json()

        rows: List[Dict[str, Any]] = []

        for event in data:
            home = event.get("home_team", "Home")
            away = event.get("away_team", "Away")
            matchup = f"{home} vs {away}"

            for book in event.get("bookmakers", []):
                book_title = book.get("title", book.get("key", "Book"))
                book_last_update = book.get("last_update") or event.get(
                    "commence_time"
                )

                for market in book.get("markets", []):
                    if market.get("key") != "h2h":
                        continue
                    for outcome in market.get("outcomes", []):
                        fighter_name = outcome.get("name", "Fighter")
                        price = outcome.get("price", None)
                        implied = american_to_implied_prob(price)

                        rows.append(
                            {
                                "matchup": matchup,
                                "book": book_title,
                                "team": fighter_name,
                                "price": price,
                                "implied": implied,
                                "last_update_iso": book_last_update,
                            }
                        )

        df = pd.DataFrame(rows)
        last_fetched_iso = now_utc().isoformat()
        return df, last_fetched_iso

    except Exception:
        fallback = [
            {
                "matchup": "Sample Fighter A vs Sample Fighter B",
                "book": "SampleBook",
                "team": "Sample Fighter A",
                "price": -150,
                "implied": american_to_implied_prob(-150),
                "last_update_iso": now_utc().isoformat(),
            },
            {
                "matchup": "Sample Fighter A vs Sample Fighter B",
                "book": "SampleBook",
                "team": "Sample Fighter B",
                "price": +130,
                "implied": american_to_implied_prob(130),
                "last_update_iso": now_utc().isoformat(),
            },
        ]
        df = pd.DataFrame(fallback)
        return df, now_utc().isoformat()


class OddsService:
    def fetch_odds(self) -> Tuple[pd.DataFrame, Optional[str]]:
        return _fetch_odds_from_api_cached()


# ---------------------------------------------------------
# Events (Tapology via RapidAPI) + geocoding
# ---------------------------------------------------------
@st.cache_data(ttl=1800)
def _fetch_upcoming_events_cached(region_id: str) -> List[Dict[str, Any]]:
    try:
        url = f"https://unofficial-tapology-api.p.rapidapi.com/api/schedule/events/{region_id}"
        headers = {
            "x-rapidapi-key": st.secrets["api"]["rapidapi_key"],
            "x-rapidapi-host": "unofficial-tapology-api.p.rapidapi.com",
        }
        r = requests.get(url, headers=headers, timeout=12)
        r.raise_for_status()
        return r.json().get("events", [])
    except Exception:
        return []


@st.cache_data(ttl=86400)
def _geocode_city_cached(city: str) -> Optional[Tuple[float, float]]:
    try:
        url = "https://nominatim.openstreetmap.org/search"
        params = {"q": city, "format": "json", "limit": 1}
        r = requests.get(
            url,
            params=params,
            timeout=10,
            headers={"User-Agent": "TapIN-MMA/1.0"},
        )
        r.raise_for_status()
        js = r.json()
        if js:
            return float(js[0]["lat"]), float(js[0]["lon"])
    except Exception:
        return None
    return None


class EventsService:
    def __init__(self):
        # Tapology region IDs (via RapidAPI)
        self.region_map = {
            # --- US States ---
            "US - Ohio": "13",
            "US - Florida": "14",
            "US - Utah": "15",
            "US - Nevada": "16",
            "US - California": "17",
            "US - Pennsylvania": "18",
            "US - Kentucky": "21",
            "US - Indiana": "22",
            "US - Tennessee": "23",
            "US - Kansas & Missouri": "24",
            "US - Iowa": "25",
            "US - Colorado": "26",
            "US - Illinois": "27",
            "US - North Carolina": "28",
            "US - New Mexico": "29",
            "US - Oklahoma": "35",
            "US - New York": "37",
            "US - New Jersey": "38",
            "US - Hawaii": "39",
            "US - Georgia": "45",
            "US - Alabama": "46",
            "US - Mississippi": "47",
            "US - Louisiana": "48",
            "US - Minnesota": "49",
            "US - North & South Dakota": "50",
            "US - Alaska": "52",
            "US - Arizona": "56",
            "US - Virginia": "57",
            "US - Wisconsin": "58",
            "US - Montana": "59",
            "US - Delaware": "61",
            "US - Maryland": "62",
            "US - Wyoming": "72",
            "US - Idaho": "73",
            "US - West Virginia": "76",

            # --- US Regions ---
            "US Region - Pacific Northwest": "11",
            "US Region - Northeast": "60",
            "US Region - Midwest": "63",
            "US Region - Southeast": "65",
            "US Region - Southwest": "68",
            "US Region - West": "69",

            # --- Canada ---
            "Canada - Québec": "1",
            "Canada (All)": "4",
            "Canada - Ontario": "42",
            "Canada - Western Canada": "43",
            "Canada - Maritime Canada": "44",

            # --- Europe ---
            "Europe - United Kingdom & Ireland": "3",
            "Europe - Nordic": "7",
            "Europe - Germany": "9",
            "Europe - Poland": "19",
            "Europe - Western": "74",
            "Europe - France": "75",
            "Europe - Italy": "77",
            "Europe - Spain": "78",
            "Europe - Netherlands": "79",
            "Europe - Austria": "80",
            "Europe - Switzerland": "81",
            "Europe - Portugal": "83",
            "Europe - Balkans": "84",
            "Europe - Greece": "85",
            "Europe - Eastern": "86",
            "Europe - Romania": "53",
            "Europe - Croatia": "54",
            "Europe - Czech Republic": "55",

            # --- Asia ---
            "Asia - Japan": "8",
            "Asia - Southeast": "12",
            "Asia - South Korea": "33",
            "Asia - China": "34",
            "Asia - Central": "92",
            "Asia - South": "93",
            "Asia - Pacific": "119",

            # --- Australia & Oceania ---
            "Australia & New Zealand": "5",

            # --- Latin America ---
            "Latin America - Brazil": "6",
            "Latin America - Peru": "32",
            "Latin America - Mexico": "40",
            "Latin America - Argentina": "70",
            "Latin America (All)": "71",
            "Latin America - Chile": "98",
            "Latin America - Colombia": "101",
            "Latin America - Guatemala": "102",
            "Latin America - Belize": "104",
            "Latin America - El Salvador": "105",
            "Latin America - Nicaragua": "106",
            "Latin America - Costa Rica": "107",
            "Latin America - Panama": "108",

            # --- Brazil Regions ---
            "Brazil - Minas Gerais": "95",
            "Brazil - Northeast": "112",
            "Brazil - North": "113",
            "Brazil - Central-West": "114",
            "Brazil - Southeast": "115",
            "Brazil - South": "116",

            # --- Middle East ---
            "Middle East (All)": "41",
            "Middle East - Bahrain": "88",
            "Middle East - Lebanon": "89",

            # --- Africa ---
            "Africa - South Africa": "36",
            "Africa - Egypt": "64",
            "Africa - North Africa": "96",

            # --- Other Subregions / Continents ---
            "Caribbean": "67",
            "North America (All)": "90",
            "Caucasus": "94",
            "Europe (All)": "117",
            "Eurasia": "118",
            "Central & South America (All)": "120",
            "Middle East & Africa (All)": "121",

        }

    def fetch_upcoming_events(self, region_id: str) -> List[Dict[str, Any]]:
        return _fetch_upcoming_events_cached(region_id)

    def geocode_city(self, city: str) -> Optional[Tuple[float, float]]:
        return _geocode_city_cached(city)


# ---------------------------------------------------------
# Email subscribers CSV + Mailjet sending
# ---------------------------------------------------------
DATA_DIR = "data"
os.makedirs(DATA_DIR, exist_ok=True)
SUBSCRIBERS_CSV = os.path.join(DATA_DIR, "subscribers.csv")


def _load_subscribers() -> pd.DataFrame:
    try:
        return pd.read_csv(SUBSCRIBERS_CSV)
    except Exception:
        return pd.DataFrame(
            columns=["email", "consent", "prefs", "subscribed", "created_at"]
        )


def _save_subscribers(df: pd.DataFrame) -> None:
    df.to_csv(SUBSCRIBERS_CSV, index=False)


class EmailService:
    def subscribe_email(self, email: str, consent: bool) -> None:
        df = _load_subscribers()
        email_norm = email.strip().lower()
        prefs: Dict[str, Any] = {}

        if "email" in df and email_norm in df["email"].values:
            df.loc[df["email"] == email_norm, ["consent", "prefs", "subscribed"]] = [
                consent,
                json.dumps(prefs),
                True,
            ]
        else:
            df.loc[len(df)] = [
                email_norm,
                consent,
                json.dumps(prefs),
                True,
                datetime.utcnow().isoformat(),
            ]
        _save_subscribers(df)

    def unsubscribe_email(self, email: str) -> bool:
        df = _load_subscribers()
        email_norm = email.strip().lower()
        if "email" in df and email_norm in df["email"].values:
            df.loc[df["email"] == email_norm, "subscribed"] = False
            _save_subscribers(df)
            return True
        return False

    def render_email_preview(self, events: List[Dict[str, Any]]) -> str:
        lines = ["TapIN MMA — Upcoming Fights Digest", "", "Highlights:"]
        for ev in events[:8]:
            lines.append(
                f"- {ev.get('organization', 'Org')} | {ev.get('datetime', 'Date')} | "
                f"{ev.get('city', 'City')} | Main: {ev.get('main_event', 'TBD')}"
            )
        lines.append("")
        lines.append("You subscribed at TapIN MMA. Unsubscribe anytime inside the app.")
        return "\n".join(lines)

    def send_email_mailjet(self, to_email: str, subject: str, content: str) -> Tuple[bool, str]:
        try:
            api_section = st.secrets["api"]
            api_key = api_section.get("mailjet_api_key")
            api_secret = api_section.get("mailjet_api_secret") or api_section.get(
                "mailjet_api_key_secret"
            )
            from_email = api_section.get("email_from") or "no-reply@tapinmma.app"
        except Exception:
            return False, "Mailjet keys not configured"

        if not api_key or not api_secret:
            return False, "Mailjet API key/secret missing"

        url = "https://api.mailjet.com/v3.1/send"

        payload = {
            "Messages": [
                {
                    "From": {"Email": from_email, "Name": "TapIN MMA"},
                    "To": [{"Email": to_email}],
                    "Subject": subject,
                    "TextPart": content,
                }
            ]
        }

        try:
            resp = requests.post(url, auth=(api_key, api_secret), json=payload, timeout=10)
            ok = 200 <= resp.status_code < 300
            return ok, ("sent" if ok else f"HTTP {resp.status_code}: {resp.text}")
        except Exception as e:
            return False, str(e)

    def send_digest_to_all(self, events: List[Dict[str, Any]]) -> Dict[str, Any]:
        df = _load_subscribers()
        out = {"sent": 0, "failed": 0, "errors": []}
        if df.empty:
            return out

        content = self.render_email_preview(events)
        for _, row in df[df.get("subscribed", True) == True].iterrows():  # noqa: E712
            ok, msg = self.send_email_mailjet(
                row["email"], "TapIN MMA — Upcoming Events", content
            )
            if ok:
                out["sent"] += 1
            else:
                out["failed"] += 1
                out["errors"].append({"email": row["email"], "error": msg})
        return out


# ---------------------------------------------------------
# Scheduler for daily digests
# ---------------------------------------------------------
def maybe_schedule_daily_mailjob(events_service: EventsService, email_service: EmailService):
    if BackgroundScheduler is None:
        return None
    if "scheduler" in st.session_state:
        return st.session_state["scheduler"]

    sched = BackgroundScheduler(daemon=True)

    def job():
        try:
            region_id = "69"
            events = events_service.fetch_upcoming_events(region_id)
            email_service.send_digest_to_all(events)
        except Exception:
            pass

    try:
        sched.add_job(job, "cron", hour=9, minute=0)
        sched.start()
        st.session_state["scheduler"] = sched
        return sched
    except Exception:
        return None
