from __future__ import annotations

import math
import os
import random
import time
from datetime import datetime, timedelta, timezone
from typing import Optional, Dict, Any

from fastapi import FastAPI, HTTPException, Request, Depends
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse, PlainTextResponse
import jwt  # PyJWT

from db import DB, init_db
from supervisor import SupervisorSystem

APP_NAME = "Poseidon Casino (Python API)"
JWT_SECRET = os.getenv("POSEIDON_JWT_SECRET", "poseidon_secret_key_2024")
JWT_ALG = "HS256"
JWT_TTL_HOURS = 2

app = FastAPI(title=APP_NAME)

# If you host PHP + API on the same domain you can lock this down to that domain.
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

db = DB(os.getenv("POSEIDON_DB_PATH", os.path.join(os.path.dirname(__file__), "data", "poseidon_casino.db")))
init_db(db)

# In-memory supervisor state (same idea as the C++ version).
player_supervisors: Dict[str, SupervisorSystem] = {}

def _now_utc() -> datetime:
    return datetime.now(timezone.utc)


def _parse_house_edge(raw_value: Any) -> float:
    """Parse house edge from admin settings.

    Supports two input styles:
    - Fractional: 0.05 == 5%
    - Percent: 5 == 5%, 20 == 20%, -100 == -100%

    Returns a clamped fraction in [-1.0, 1.0].
    """
    try:
        v = float(raw_value)
    except Exception:
        return 0.0

    # If user enters values like 20 or -100, treat as percent.
    if abs(v) > 1.0:
        v = v / 100.0

    # Full spectrum clamp: -100%..+100%
    if v > 1.0:
        v = 1.0
    if v < -1.0:
        v = -1.0
    return v


def _get_game_settings(game: str) -> Dict[str, Any]:
    # Read from shared SQLite table written by the admin panel
    s = db.get_game_settings(game)

    # House edge is stored in s["house_edge"]. We accept full spectrum:
    # -1.0 => player always wins (100% player edge)
    #  0.0 => fair (no edge)
    # +1.0 => house always wins (100% house edge)
    s["house_edge"] = _parse_house_edge(s.get("house_edge", 0.0))
    return s

def _enforce_game_rules(game: str, bet: float) -> Dict[str, Any]:
    s = _get_game_settings(game)
    if int(s.get("enabled", 1)) != 1:
        raise HTTPException(status_code=403, detail=f"{game.capitalize()} is currently disabled")
    min_bet = float(s.get("min_bet") or 0.0)
    max_bet = float(s.get("max_bet") or 0.0)
    if min_bet > 0 and bet < min_bet:
        raise HTTPException(status_code=400, detail=f"Bet must be at least {min_bet:g}")
    if max_bet > 0 and bet > max_bet:
        raise HTTPException(status_code=400, detail=f"Bet must be at most {max_bet:g}")
    return s

def create_jwt(user_id: int, username: str) -> str:
    exp = _now_utc() + timedelta(hours=JWT_TTL_HOURS)
    payload = {"user_id": user_id, "username": username, "exp": exp}
    return jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALG)

def verify_jwt(token: str) -> Dict[str, Any]:
    try:
        payload = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALG])
        return payload
    except jwt.ExpiredSignatureError:
        raise HTTPException(status_code=401, detail="Token expired")
    except jwt.InvalidTokenError:
        raise HTTPException(status_code=401, detail="Invalid token")

def get_bearer_token(req: Request) -> str:
    auth = req.headers.get("Authorization", "")
    if not auth.startswith("Bearer "):
        raise HTTPException(status_code=401, detail="No token provided")
    return auth[7:]

def auth_user(req: Request) -> Dict[str, Any]:
    token = get_bearer_token(req)
    return verify_jwt(token)

@app.post("/api/register")
async def register(payload: Dict[str, Any]):
    username = (payload.get("username") or "").strip()
    password = (payload.get("password") or "")
    if not username or not password:
        raise HTTPException(status_code=400, detail="username and password required")

    if db.user_exists(username):
        return JSONResponse(status_code=409, content={"error": "Username already exists"})

    user_id = db.create_user(username=username, password=password)
    db.ensure_balance_row(user_id)

    token = create_jwt(user_id, username)
    return {"message": "User registered", "token": token, "username": username}

@app.post("/api/login")
async def login(payload: Dict[str, Any]):
    username = (payload.get("username") or "").strip()
    password = (payload.get("password") or "")
    if not username or not password:
        raise HTTPException(status_code=400, detail="username and password required")

    user = db.verify_user(username, password)
    if not user:
        return JSONResponse(status_code=401, content={"error": "Invalid credentials"})

    user_id = int(user["id"])
    db.ensure_balance_row(user_id)
    token = create_jwt(user_id, username)
    return {"message": "Login successful", "token": token, "username": username}

@app.get("/api/balance")
async def balance(user=Depends(auth_user)):
    user_id = int(user["user_id"])
    bal = db.get_balance(user_id)
    return {"balance": bal}

@app.post("/api/add-funds")
async def add_funds(payload: Dict[str, Any], user=Depends(auth_user)):
    user_id = int(user["user_id"])
    try:
        amount = float(payload.get("amount"))
    except Exception:
        raise HTTPException(status_code=400, detail="Invalid amount")
    if amount <= 0:
        raise HTTPException(status_code=400, detail="Amount must be positive")

    db.add_balance(user_id, amount)
    db.insert_transaction(user_id=user_id, amount=amount, tx_type="deposit", game="funds")

    return {"message": "Funds added", "balance": db.get_balance(user_id)}

@app.post("/api/games/dice")
async def play_dice(payload: Dict[str, Any], user=Depends(auth_user)):
    user_id = int(user["user_id"])
    username = str(user.get("username") or payload.get("username") or "")
    try:
        bet = float(payload.get("bet"))
        target = int(payload.get("target"))
    except Exception:
        raise HTTPException(status_code=400, detail="Invalid bet/target")

    if bet <= 0:
        raise HTTPException(status_code=400, detail="Bet must be positive")
    if target < 2 or target > 12:
        raise HTTPException(status_code=400, detail="Target must be 2..12")

    # Load admin-configured settings (enabled/min/max/house edge)
    settings = _enforce_game_rules('dice', bet)
    house_edge = float(settings.get('house_edge') or 0.0)

    bal = db.get_balance(user_id)
    if bet > bal:
        raise HTTPException(status_code=400, detail="Insufficient balance")

        # Load admin-configured settings (enabled/min/max/house edge)
        settings = _enforce_game_rules('rocket', bet)
        house_edge = float(settings.get('house_edge') or 0.0)

    # Supervisor system
    supervisor = player_supervisors.setdefault(username, SupervisorSystem())

    # Base probability for sum of two dice:
    # counts: 2:1,3:2,4:3,5:4,6:5,7:6,8:5,9:4,10:3,11:2,12:1
    counts = {2:1,3:2,4:3,5:4,6:5,7:6,8:5,9:4,10:3,11:2,12:1}
    base_probability = counts[target] / 36.0
    adjusted_probability = supervisor.adjust_probability(base_probability, bet)

    # Full-spectrum edge overrides:
    # -100% (edge <= -1): player always wins
    # +100% (edge >= +1): house always wins
    if house_edge <= -0.999999:
        adjusted_probability = 1.0
    elif house_edge >= 0.999999:
        adjusted_probability = 0.0

    # Roll dice
    dice1 = random.randint(1, 6)
    dice2 = random.randint(1, 6)
    s = dice1 + dice2

    should_win = random.random() < adjusted_probability

    # Force/avoid outcome similar to the C++ approach
    if should_win and s != target:
        dice1 = min(target - 1, 6)
        dice2 = target - dice1
        dice2 = max(1, min(dice2, 6))
        s = dice1 + dice2
    elif (not should_win) and s == target:
        dice1 = (dice1 % 6) + 1
        s = dice1 + dice2

    won = (s == target)
    # Real house edge: choose a payout multiplier so the expected value is -(house_edge * bet)
    # For a given target sum, base_probability is the true odds (ways/36).
    # With return_multiplier R, EV = bet * (base_probability * R - 1). Set R = (1 - house_edge) / base_probability.
    # Note: the supervisor may bias win/loss frequency for retention; house_edge controls the payout table.
    return_multiplier = (1.0 - house_edge) / max(base_probability, 1e-9)
    # Total return on win (includes original bet); on loss we show -bet (same as before)
    payout = (bet * return_multiplier) if won else -bet

    # Update supervisor streaks
    if won:
        supervisor.consecutive_wins += 1
        supervisor.consecutive_losses = 0
    else:
        supervisor.consecutive_losses += 1
        supervisor.consecutive_wins = 0

    # Apply to balance
    if won:
        db.add_balance(user_id, payout - bet)  # net profit
        db.insert_transaction(user_id, payout - bet, "win", "dice")
    else:
        db.add_balance(user_id, -bet)
        db.insert_transaction(user_id, -bet, "loss", "dice")

    return {
        "dice1": dice1,
        "dice2": dice2,
        "sum": s,
        "won": won,
        "payout": payout,
        "balance": db.get_balance(user_id),
    }

@app.post("/api/games/rocket")
async def play_rocket(payload: Dict[str, Any], user=Depends(auth_user)):
    user_id = int(user["user_id"])
    username = str(user.get("username") or payload.get("username") or "")
    action = str(payload.get("action") or "").lower().strip()
    if action not in {"start", "cashout"}:
        raise HTTPException(status_code=400, detail="Invalid action")

    supervisor = player_supervisors.setdefault(username, SupervisorSystem())

    if action == "start":
        try:
            bet = float(payload.get("bet"))
        except Exception:
            raise HTTPException(status_code=400, detail="Invalid bet")
        if bet <= 0:
            raise HTTPException(status_code=400, detail="Bet must be positive")

        # Load admin-configured settings (enabled/min/max/house edge)
        settings = _enforce_game_rules("rocket", bet)
        house_edge = float(settings.get("house_edge", 0.0))

        bal = db.get_balance(user_id)
        if bet > bal:
            raise HTTPException(status_code=400, detail="Insufficient balance")

        # Deduct bet immediately
        db.add_balance(user_id, -bet)

        # Generate crash point with a controllable full-spectrum house edge.
        # Edge interpretation:
        #   -1.0 => player always wins
        #    0.0 => fair
        #   +1.0 => house always wins
        if house_edge <= -0.999999:
            crash_point = 1e9
        elif house_edge >= 0.999999:
            crash_point = 1.01
        else:
            # Classic crash math: P(reach multiplier m) = (1 - e) / m
            # Equivalent generation: crash = (1 - e) / u  where u ~ Uniform(0,1)
            u = random.random()
            if u <= 0.0:
                u = 1e-12
            raw_crash = (1.0 - house_edge) / u

            # Keep the existing "supervisor" streak bias (retention shaping).
            if supervisor.consecutive_losses > 3:
                raw_crash *= 1.5
            elif supervisor.consecutive_wins > 2:
                raw_crash *= 0.7

            # Avoid absurd values while still allowing negative edges to be noticeable.
            crash_point = max(1.01, min(raw_crash, 1000.0))

        return {"crash_point": crash_point, "balance": db.get_balance(user_id)}

    # cashout
    try:
        bet = float(payload.get("bet"))
        multiplier = float(payload.get("multiplier"))
        crash_point = float(payload.get("crash_point") or payload.get("crashPoint"))
    except Exception:
        raise HTTPException(status_code=400, detail="Invalid cashout payload")

    if bet <= 0 or multiplier <= 0:
        raise HTTPException(status_code=400, detail="Invalid bet/multiplier")

    # Load settings again (mainly for house edge full-spectrum overrides)
    settings = _enforce_game_rules("rocket", bet)
    house_edge = float(settings.get("house_edge", 0.0))

    # Full-spectrum overrides
    if house_edge <= -0.999999:
        won = True
    elif house_edge >= 0.999999:
        won = False
    else:
        won = multiplier < crash_point

    payout = bet * multiplier if won else 0.0

    if won:
        supervisor.consecutive_wins += 1
        supervisor.consecutive_losses = 0
    else:
        supervisor.consecutive_losses += 1
        supervisor.consecutive_wins = 0

    if won:
        db.add_balance(user_id, payout)
        db.insert_transaction(user_id, payout - bet, "win", "rocket")
    # If lost, bet was already deducted on start; record loss:
    else:
        db.insert_transaction(user_id, -bet, "loss", "rocket")

    return {"won": won, "payout": payout, "balance": db.get_balance(user_id), "crashed_at": crash_point}

@app.options("/api/{path:path}")
async def preflight(path: str):
    return PlainTextResponse("OK")

#x