import os
from dotenv import load_dotenv
load_dotenv()

import requests
import pandas as pd
from datetime import datetime, timedelta, timezone

# =========================================================
# CONFIG
# =========================================================

API_KEY = os.environ.get("ODDS_API_KEY")

BASE_URL = "https://api.the-odds-api.com/v4"
REGIONS = "us,us2"
MARKETS = "h2h"
ODDS_FORMAT = "american"
DATE_FORMAT = "iso"

FREE_BET_STAKE = 1.0
HOURS_AHEAD = 168

TOP_PRINT = 5
SHORTLIST_SIZE = 10

OUTPUT_DIR = r"C:\Users .... INSERT YOUR OUTPUT DIRECTORY HERE"
OUTPUT_FILE = os.path.join(OUTPUT_DIR, f"top_freebet_hedges_fresh_american_next_{HOURS_AHEAD}h.csv")


SPORT_KEYS = [
    "baseball_mlb",
    "baseball_ncaa",
    "basketball_nba",
    "basketball_wnba",
    "basketball_ncaab",
    "basketball_wncaab",
    "basketball_euroleague",
    "icehockey_nhl",
    "mma_mixed_martial_arts",
    "cricket_ipl",
    "aussierules_afl",
    "americanfootball_nfl",
    "americanfootball_ncaaf",
    "americanfootball_cfl",
    "americanfootball_ufl",
    "boxing_boxing",
]

ALLOWED_BOOKMAKERS = {
    "FanDuel",
    "DraftKings",
    "BetMGM",
}

# =========================================================
# HELPERS
# =========================================================

def iso_z(dt: datetime) -> str:
    return dt.astimezone(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")


def parse_time(ts: str | None) -> datetime | None:
    if not ts:
        return None
    return datetime.fromisoformat(ts.replace("Z", "+00:00"))


def american_to_decimal(american_odds: float) -> float:
    if american_odds > 0:
        return 1.0 + american_odds / 100.0
    return 1.0 + 100.0 / abs(american_odds)


def format_american(american_odds: float) -> str:
    american_odds = int(round(american_odds))
    return f"+{american_odds}" if american_odds > 0 else str(american_odds)


def update_ago_text(last_update: str | None, now_utc: datetime) -> str:
    dt = parse_time(last_update)
    if dt is None:
        return ""

    seconds = max(0, int((now_utc - dt).total_seconds()))

    if seconds < 60:
        return f"{seconds} seconds ago"

    minutes = seconds // 60
    if minutes < 60:
        return f"{minutes} minutes ago"

    hours = minutes // 60
    return f"{hours} hours ago"


def get_available_sports(api_key: str) -> list[dict]:
    url = f"{BASE_URL}/sports/"
    params = {"apiKey": api_key, "all": "true"}
    r = requests.get(url, params=params, timeout=30)
    r.raise_for_status()
    return r.json()


def fetch_odds_for_sport(
    api_key: str,
    sport_key: str,
    commence_from: datetime,
    commence_to: datetime,
) -> tuple[list[dict], dict]:
    url = f"{BASE_URL}/sports/{sport_key}/odds/"
    params = {
        "apiKey": api_key,
        "regions": REGIONS,
        "markets": MARKETS,
        "oddsFormat": ODDS_FORMAT,
        "dateFormat": DATE_FORMAT,
        "commenceTimeFrom": iso_z(commence_from),
        "commenceTimeTo": iso_z(commence_to),
    }

    r = requests.get(url, params=params, timeout=30)
    r.raise_for_status()

    headers = {
        "x-requests-last": r.headers.get("x-requests-last"),
        "x-requests-used": r.headers.get("x-requests-used"),
        "x-requests-remaining": r.headers.get("x-requests-remaining"),
    }

    return r.json(), headers


def fetch_fresh_event_odds(api_key: str, sport_key: str, event_id: str) -> tuple[dict, dict]:
    url = f"{BASE_URL}/sports/{sport_key}/events/{event_id}/odds"
    params = {
        "apiKey": api_key,
        "regions": REGIONS,
        "markets": MARKETS,
        "oddsFormat": ODDS_FORMAT,
        "dateFormat": DATE_FORMAT,
    }

    r = requests.get(url, params=params, timeout=30)
    r.raise_for_status()

    headers = {
        "x-requests-last": r.headers.get("x-requests-last"),
        "x-requests-used": r.headers.get("x-requests-used"),
        "x-requests-remaining": r.headers.get("x-requests-remaining"),
    }

    return r.json(), headers


def extract_best_prices(event: dict) -> dict:
    best = {}

    for bookmaker in event.get("bookmakers", []):
        bookmaker_title = bookmaker.get("title", bookmaker.get("key", "Unknown"))

        if bookmaker_title not in ALLOWED_BOOKMAKERS:
            continue

        for market in bookmaker.get("markets", []):
            if market.get("key") != "h2h":
                continue

            market_last_update = market.get("last_update") or bookmaker.get("last_update")

            for outcome in market.get("outcomes", []):
                name = outcome.get("name")
                price = outcome.get("price")

                if name is None or price is None:
                    continue

                try:
                    american_price = float(price)
                except (TypeError, ValueError):
                    continue

                decimal_price = american_to_decimal(american_price)

                if decimal_price <= 1.0:
                    continue

                if name not in best or decimal_price > best[name]["decimal_price"]:
                    best[name] = {
                        "american_display": format_american(american_price),
                        "decimal_price": decimal_price,
                        "bookmaker": bookmaker_title,
                        "last_update": market_last_update,
                    }

    return best


def free_bet_hedge(
    free_bet_odds_decimal: float,
    hedge_odds_decimal: float,
    free_bet_stake: float = 1.0,
) -> tuple[float, float]:
    hedge_stake = free_bet_stake * (free_bet_odds_decimal - 1.0) / hedge_odds_decimal
    guaranteed_profit = free_bet_stake * (free_bet_odds_decimal - 1.0) - hedge_stake
    return hedge_stake, guaranteed_profit


def dedupe_events(events: list[dict]) -> list[dict]:
    seen = set()
    out = []

    for ev in events:
        ev_id = ev.get("id")
        if ev_id and ev_id not in seen:
            out.append(ev)
            seen.add(ev_id)

    return out


def rows_from_event(event: dict, as_of_utc: datetime) -> list[dict]:
    rows = []
    best = extract_best_prices(event)

    if len(best) != 2:
        return rows

    outcome_names = list(best.keys())
    a_name, b_name = outcome_names[0], outcome_names[1]

    commence = parse_time(event.get("commence_time"))

    for free_name, hedge_name in [(a_name, b_name), (b_name, a_name)]:
        free_decimal = best[free_name]["decimal_price"]
        hedge_decimal = best[hedge_name]["decimal_price"]

        hedge_stake, guaranteed_profit = free_bet_hedge(
            free_bet_odds_decimal=free_decimal,
            hedge_odds_decimal=hedge_decimal,
            free_bet_stake=FREE_BET_STAKE,
        )

        rows.append({
            "sport_key": event.get("sport_key"),
            "sport_title": event.get("sport_title"),
            "event_id": event.get("id"),
            "home_team": event.get("home_team"),
            "away_team": event.get("away_team"),
            "commence_time_utc": commence.isoformat() if commence else event.get("commence_time"),

            "free_bet_on": free_name,
            "free_bet_american_odds": best[free_name]["american_display"],
            "free_bet_bookmaker": best[free_name]["bookmaker"],
            "free_bet_last_update": best[free_name]["last_update"],
            "free_bet_update_ago": update_ago_text(best[free_name]["last_update"], as_of_utc),

            "hedge_on": hedge_name,
            "hedge_american_odds": best[hedge_name]["american_display"],
            "hedge_bookmaker": best[hedge_name]["bookmaker"],
            "hedge_last_update": best[hedge_name]["last_update"],
            "hedge_update_ago": update_ago_text(best[hedge_name]["last_update"], as_of_utc),

            "free_bet_stake": FREE_BET_STAKE,
            "hedge_stake": hedge_stake,
            "guaranteed_profit": guaranteed_profit,
            "conversion_rate": guaranteed_profit / FREE_BET_STAKE,
            "recommendation_refreshed_at_utc": as_of_utc.isoformat(),
        })

    return rows


def build_opportunity_rows(events: list[dict], as_of_utc: datetime) -> list[dict]:
    rows = []
    for event in events:
        rows.extend(rows_from_event(event, as_of_utc))
    return rows


def build_sport_keys_to_query(available_keys: set[str]) -> list[str]:
    fixed_keys = [k for k in SPORT_KEYS if k in available_keys]

    tennis_keys = sorted(
        k for k in available_keys
        if k.startswith("tennis_")
    )

    return fixed_keys + tennis_keys


# =========================================================
# MAIN
# =========================================================

def main() -> None:
    if not API_KEY:
        raise ValueError("ODDS_API_KEY not found in environment.")

    now_utc = datetime.now(timezone.utc)
    end_utc = now_utc + timedelta(hours=HOURS_AHEAD)

    available_sports = get_available_sports(API_KEY)
    available_keys = {s["key"] for s in available_sports}
    sport_keys_to_query = build_sport_keys_to_query(available_keys)

    if not sport_keys_to_query:
        print("None of the selected sport keys are available from the API.")
        return

    print("Using bookmakers:")
    print(", ".join(sorted(ALLOWED_BOOKMAKERS)))
    print()

    print("Initial scan of sports:\n")

    all_events = []
    coverage_rows = []
    total_credits_used_this_run = 0
    last_seen_used_total = None
    last_seen_remaining = None

    for sport_key in sport_keys_to_query:
        try:
            events, header_info = fetch_odds_for_sport(
                api_key=API_KEY,
                sport_key=sport_key,
                commence_from=now_utc,
                commence_to=end_utc,
            )
        except requests.HTTPError as e:
            print(f"Skipping {sport_key} due to HTTP error: {e}")
            continue
        except requests.RequestException as e:
            print(f"Skipping {sport_key} due to request error: {e}")
            continue

        credits_last = int(header_info.get("x-requests-last", "0") or 0)
        total_credits_used_this_run += credits_last
        last_seen_used_total = header_info.get("x-requests-used")
        last_seen_remaining = header_info.get("x-requests-remaining")

        all_events.extend(events)

        coverage_rows.append({
            "sport_key": sport_key,
            "events_returned": len(events),
        })

        print(
            f"{sport_key}: returned {len(events)} events, "
            f"credits this call = {credits_last}"
        )

    if not all_events:
        print(f"No events found in the next {HOURS_AHEAD} hours.")
        return

    all_events = dedupe_events(all_events)

    initial_as_of = datetime.now(timezone.utc)
    initial_rows = build_opportunity_rows(all_events, initial_as_of)

    if not initial_rows:
        print("Events were found, but none had usable 2-outcome h2h odds from the allowed bookmakers.")
        return

    initial_df = pd.DataFrame(initial_rows).sort_values(
        by=["guaranteed_profit"],
        ascending=[False],
    ).reset_index(drop=True)

    shortlist_events = (
        initial_df[["sport_key", "event_id"]]
        .drop_duplicates()
        .head(SHORTLIST_SIZE)
        .to_dict("records")
    )

    print()
    print(f"Rechecking the top {len(shortlist_events)} candidate events using single-event odds...")
    print()

    fresh_events = []

    for item in shortlist_events:
        try:
            fresh_event, header_info = fetch_fresh_event_odds(
                api_key=API_KEY,
                sport_key=item["sport_key"],
                event_id=item["event_id"],
            )
        except requests.HTTPError as e:
            print(f"Skipping refreshed event {item['event_id']} due to HTTP error: {e}")
            continue
        except requests.RequestException as e:
            print(f"Skipping refreshed event {item['event_id']} due to request error: {e}")
            continue

        credits_last = int(header_info.get("x-requests-last", "0") or 0)
        total_credits_used_this_run += credits_last
        last_seen_used_total = header_info.get("x-requests-used")
        last_seen_remaining = header_info.get("x-requests-remaining")

        fresh_events.append(fresh_event)

    fresh_as_of = datetime.now(timezone.utc)
    fresh_rows = build_opportunity_rows(fresh_events, fresh_as_of)

    if not fresh_rows:
        print("The refreshed candidate events had no usable 2-outcome h2h odds from the allowed bookmakers.")
        return

    df = pd.DataFrame(fresh_rows).sort_values(
        by=["guaranteed_profit"],
        ascending=[False],
    ).reset_index(drop=True)

    top = df.head(TOP_PRINT).copy()

    pd.set_option("display.max_columns", None)
    pd.set_option("display.width", 260)
    pd.set_option("display.max_colwidth", 60)

    print(f"Top {TOP_PRINT} freshly rechecked guaranteed returns from unit SNR free bets:\n")
    print(
        top[
            [
                "sport_title",
                "commence_time_utc",
                "home_team",
                "away_team",
                "free_bet_on",
                "free_bet_american_odds",
                "free_bet_bookmaker",
                "hedge_on",
                "hedge_american_odds",
                "hedge_bookmaker",
                "free_bet_stake",
                "hedge_stake",
                "guaranteed_profit",
                "conversion_rate",
            ]
        ].to_string(index=False)
    )

    print("\n----------------------------------------")
    print(f"Total credits used in this run: {total_credits_used_this_run}")
    if last_seen_used_total is not None:
        print(f"x-requests-used after last call: {last_seen_used_total}")
    if last_seen_remaining is not None:
        print(f"x-requests-remaining after last call: {last_seen_remaining}")
    print("----------------------------------------")

    os.makedirs(OUTPUT_DIR, exist_ok=True)
    df.to_csv(OUTPUT_FILE, index=False, encoding="utf-8-sig")
    print(f"\nSaved full refreshed results to: {OUTPUT_FILE}")


if __name__ == "__main__":
    main()
    