"""
FenyaBot Mini App — FastAPI Backend.

REST API для Telegram Mini App. Использует те же SQLite базы и
бизнес-логику что и основной бот.
"""

import asyncio
import logging
import os
import hashlib
from urllib.parse import quote
from urllib.parse import urljoin
from urllib.parse import urlparse
import sys
from contextlib import asynccontextmanager
from typing import Annotated

import aiohttp
from fastapi import FastAPI, Depends, Header, HTTPException, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import Response, StreamingResponse
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel

# Добавляем корень проекта в sys.path чтобы импортировать модули бота
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

import config
from games.casino import play_casino, get_user_length, spin_slots, MULTIPLIERS, update_user_length
from games.betting import (
    fetch_matches,
    place_bet,
    get_match_by_index,
    resolve_match_outcome,
    get_user_bets as _get_user_bets_raw,
    get_user_bet_history as _get_user_bet_history_raw,
)
from webapp.auth import validate_init_data
from games.uwu import fetch_uwu_post, fetch_furtok_feed
from webapp.shorties import fetch_shorties_feed

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


# ──────────────────── Auth dependency ────────────────────

async def get_current_user(x_telegram_init_data: Annotated[str, Header()] = "") -> dict:
    """Извлекает и валидирует пользователя из заголовка X-Telegram-Init-Data."""
    user = validate_init_data(x_telegram_init_data)
    if user is None:
        raise HTTPException(status_code=401, detail="Invalid Telegram auth")
    return user


# ──────────────────── Pydantic models ────────────────────

class BetRequest(BaseModel):
    league: str
    match_index: int
    outcome: str  # "1", "X", "2"
    amount: float

class CasinoRequest(BaseModel):
    bet: float

class TalkRequest(BaseModel):
    text: str

class PenisResult(BaseModel):
    success: bool
    message: str
    length: float | None = None

class ProfileResponse(BaseModel):
    user_id: int
    first_name: str
    username: str
    length: float | None
    active_bets: int

class MatchResponse(BaseModel):
    index: int
    home: str
    away: str
    commence: str
    odds_home: float | None
    odds_draw: float | None
    odds_away: float | None

class LeagueInfo(BaseModel):
    alias: str
    name: str
    sport_key: str

class CasinoResult(BaseModel):
    reels: list[str]
    matches: int
    multiplier: float
    delta: float
    new_length: float
    won: bool

class SchedulePair(BaseModel):
    pair: int | None
    title: str
    time_start: str | None = None
    time_end: str | None = None


# ──────────────────── Lifespan ────────────────────

_http_session: aiohttp.ClientSession | None = None


def get_http_session() -> aiohttp.ClientSession:
    return _http_session


@asynccontextmanager
async def lifespan(app: FastAPI):
    global _http_session
    _http_session = aiohttp.ClientSession(
        timeout=aiohttp.ClientTimeout(total=120),
        connector=aiohttp.TCPConnector(limit=64, ttl_dns_cache=300),
    )
    logger.info("Mini App API started")
    yield
    await _http_session.close()
    logger.info("Mini App API stopped")


# ──────────────────── App ────────────────────

app = FastAPI(
    title="FenyaBot Mini App API",
    version="1.0.0",
    lifespan=lifespan,
)

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)


# ──────────────────── Endpoints ────────────────────

# --- Профиль ---

@app.get("/api/profile")
async def get_profile(user: dict = Depends(get_current_user)) -> ProfileResponse:
    """Профиль пользователя: баланс и количество активных ставок."""
    user_id = user["user_id"]
    length = await asyncio.to_thread(get_user_length, user_id)

    active_bets = 0
    try:
        from db import get_conn
        with get_conn() as conn:
            cur = conn.cursor()
            cur.execute(
                "SELECT COUNT(*) FROM bets WHERE user_id = %s AND status = 'pending'",
                (user_id,),
            )
            row = cur.fetchone()
            active_bets = row[0] if row else 0
    except Exception:
        pass

    return ProfileResponse(
        user_id=user_id,
        first_name=user["first_name"],
        username=user["username"],
        length=length,
        active_bets=active_bets,
    )


# --- Лиги ---

@app.get("/api/leagues")
async def get_leagues() -> list[LeagueInfo]:
    """Список доступных лиг для ставок."""
    labels = {
        "dota2": "🎮 Dota 2",
    }
    return [
        LeagueInfo(alias=alias, name=labels.get(alias, alias), sport_key=key)
        for alias, key in config.ODDS_SPORTS.items()
    ]


# --- Матчи ---

@app.get("/api/matches/{league}")
async def get_matches(league: str) -> list[MatchResponse]:
    """Матчи для конкретной лиги."""
    if league not in config.ODDS_SPORTS:
        raise HTTPException(status_code=404, detail="League not found")

    sport_key = config.ODDS_SPORTS[league]
    matches = await fetch_matches(sport_key)

    result = []
    for i, m in enumerate(matches):
        home = m.get("home", "?")
        away = m.get("away", "?")
        odds = m.get("odds", {})
        
        odds_home = odds.get(home)
        odds_away = odds.get(away)
        
        _DRAW_NAMES = {"draw", "tie", "ничья"}
        draw_name = next((name for name in odds if name.casefold() in _DRAW_NAMES), None)
        odds_draw = odds.get(draw_name) if draw_name else None

        result.append(MatchResponse(
            index=i + 1,
            home=home,
            away=away,
            commence=m.get("commence", ""),
            odds_home=odds_home,
            odds_draw=odds_draw,
            odds_away=odds_away,
        ))

    return result


# --- Ставки ---

@app.post("/api/bet")
async def create_bet(
    req: BetRequest,
    user: dict = Depends(get_current_user),
):
    """Разместить ставку на матч."""
    user_id = user["user_id"]

    # 1. Найти матч по индексу
    match = await get_match_by_index(req.league, req.match_index)
    if not match:
        raise HTTPException(status_code=404, detail="Матч не найден")

    # 2. Преобразовать код исхода (1/X/2) в название команды
    team = resolve_match_outcome(match, req.outcome)
    if not team:
        raise HTTPException(status_code=400, detail="Неверный исход ставки")

    # 3. Разместить ставку (синхронная функция)
    success, result = await asyncio.to_thread(place_bet, user_id, match, req.league, team, req.amount)
    if not success:
        raise HTTPException(status_code=400, detail=result)
    return {"message": result}


@app.get("/api/mybets")
async def get_my_bets(user: dict = Depends(get_current_user)):
    """Активные ставки пользователя."""
    user_id = user["user_id"]
    result = await asyncio.to_thread(_get_user_bets_raw, user_id)
    return {"text": result}


@app.get("/api/bet_history")
async def get_bet_history(user: dict = Depends(get_current_user)):
    """История ставок за месяц + статистика."""
    user_id = user["user_id"]
    result = await asyncio.to_thread(_get_user_bet_history_raw, user_id)
    return {"text": result}


# --- Казино ---

@app.post("/api/casino")
async def play_casino_api(
    req: CasinoRequest,
    user: dict = Depends(get_current_user),
) -> CasinoResult:
    """Играть в казино (слоты)."""
    import random

    user_id = user["user_id"]

    current = await asyncio.to_thread(get_user_length, user_id)
    if current is None:
        raise HTTPException(status_code=400, detail="Сначала крутни /penis")
    if current <= 0:
        raise HTTPException(status_code=400, detail="Баланс <= 0")
    if req.bet > config.CASINO_MAX_BET:
        raise HTTPException(status_code=400, detail=f"Макс ставка: {config.CASINO_MAX_BET} см")
    if req.bet <= 0:
        raise HTTPException(status_code=400, detail="Ставка > 0")
    if round(req.bet, 2) > round(current, 2):
        raise HTTPException(status_code=400, detail=f"У тебя {current:.1f} см")

    reels, match_count = await asyncio.to_thread(spin_slots)

    if match_count >= 2:
        min_mult, max_mult = MULTIPLIERS[match_count]
        multiplier = round(random.uniform(min_mult, max_mult), 1)
        winnings = round(req.bet * multiplier, 1)
        new_length = await asyncio.to_thread(update_user_length, user_id, winnings)
        return CasinoResult(
            reels=reels, matches=match_count, multiplier=multiplier,
            delta=winnings, new_length=new_length or current, won=True,
        )
    else:
        new_length = await asyncio.to_thread(update_user_length, user_id, -req.bet)
        return CasinoResult(
            reels=reels, matches=0, multiplier=0,
            delta=-req.bet, new_length=new_length or current, won=False,
        )


# --- Penis Game ---

@app.post("/api/penis")
async def play_penis_api(user: dict = Depends(get_current_user)) -> PenisResult:
    """Крутить penis (раз в 24 часа)."""
    # Импортируем из main.py
    from main import play_penis, format_penis_user_name

    user_id = user["user_id"]
    display_name = user["username"] or user["first_name"] or f"user_{user_id}"
    success, message = await asyncio.to_thread(play_penis, user_id, display_name)
    length = await asyncio.to_thread(get_user_length, user_id)
    return PenisResult(success=success, message=message, length=length)


# --- Топ ---

@app.get("/api/top")
async def get_top():
    """Лидерборд по размеру."""
    from main import build_penis_top
    result = await asyncio.to_thread(build_penis_top)
    return {"text": result}


# --- Расписание ---

@app.get("/api/schedule")
async def get_schedule(day: int | None = None) -> list[SchedulePair]:
    """Расписание пар. day = 0-6 (пн-вс), None = сегодня."""
    from datetime import datetime
    from zoneinfo import ZoneInfo

    if day is None:
        day = datetime.now(ZoneInfo(config.DEFAULT_TZ)).weekday()

    # Определяем неделю (верхняя/нижняя)
    now = datetime.now(ZoneInfo(config.DEFAULT_TZ))
    iso_week = now.isocalendar()[1]
    if config.UPPER_WEEK_IS_ODD:
        week_type = "upper" if iso_week % 2 == 1 else "lower"
    else:
        week_type = "upper" if iso_week % 2 == 0 else "lower"

    lessons = config.LESSON_SCHEDULE.get(week_type, {}).get(day, [])

    result = []
    for lesson in lessons:
        pair_num = lesson.get("pair")
        time_start = time_end = None
        if pair_num and pair_num in config.PAIR_TIMES:
            start, end = config.PAIR_TIMES[pair_num]
            time_start = f"{start.hour:02d}:{start.minute:02d}"
            time_end = f"{end.hour:02d}:{end.minute:02d}"

        result.append(SchedulePair(
            pair=pair_num,
            title=lesson.get("title", ""),
            time_start=time_start,
            time_end=time_end,
        ))

    return result


# --- UwU Арты ---

@app.get("/api/uwu")
async def get_uwu_api(
    tags: str = "rating:safe score:>500 -animated",
    user: dict = Depends(get_current_user),
):
    """Отправить случайный uwu арт по тегам."""
    try:
        tags += " order:random score:>300"
        post = await fetch_uwu_post(tags)
        return {"response": post}
    except Exception as e:
        logger.exception("UwU API failed")
        raise HTTPException(status_code=500, detail=str(e))


# --- FurTok Лента ---

@app.get("/api/furtok")
async def get_furtok_feed_api(
    safe: bool = True,
    page: int = 1,
    tags: str = "",
    user: dict = Depends(get_current_user),
):
    """Получить ленту анимированных постов для FurTok."""
    try:
        feed = await fetch_furtok_feed(safe=safe, page=page, extra_tags=tags)
        return {"feed": feed}
    except Exception as e:
        logger.exception("FurTok API failed")
        raise HTTPException(status_code=500, detail=str(e))


@app.get("/api/furtok/shorties")
async def get_shorties_feed_api(
    page: int = 1,
    count: int = 8,
    user: dict = Depends(get_current_user),
):
    """Случайная shorties-лента с Pornhub."""
    try:
        # page используем как "seed-сдвиг" для подгрузки следующих рандомных наборов.
        random_pages = 2 + min(max(page, 1), 6)
        feed = await fetch_shorties_feed(count=count, random_pages=random_pages)
        return {"feed": feed}
    except Exception as e:
        logger.exception("Shorties API failed")
        raise HTTPException(status_code=500, detail=str(e))


@app.get("/api/proxy-image")
async def proxy_image(url: str):
    """Проксирует картинку с e621 чтобы обойти hotlink protection."""
    allowed = ("static1.e621.net", "static2.e621.net", "static1.e926.net")
    from urllib.parse import urlparse
    parsed = urlparse(url)
    if parsed.hostname not in allowed:
        raise HTTPException(status_code=403, detail="Forbidden host")

    headers = {
        "User-Agent": f"FenyaBot/1.0 (by {config.E621_LOGIN or 'unknown'} on e621)",
    }
    try:
        async with aiohttp.ClientSession() as session:
            async with session.get(url, headers=headers, timeout=aiohttp.ClientTimeout(total=15)) as resp:
                if resp.status != 200:
                    raise HTTPException(status_code=resp.status, detail="Upstream error")
                content_type = resp.content_type or "image/png"
                body = await resp.read()
                return Response(
                    content=body,
                    media_type=content_type,
                    headers={"Cache-Control": "public, max-age=86400"},
                )
    except aiohttp.ClientError:
        raise HTTPException(status_code=502, detail="Failed to fetch image")


def _is_allowed_shorties_media_host(hostname: str) -> bool:
    host = (hostname or "").lower()
    if not host:
        return False
    if host in {"www.pornhub.com", "pornhub.com", "phncdn.com"}:
        return True
    if host.endswith(".phncdn.com"):
        return True
    return False


@app.get("/api/proxy-shorties-media")
async def proxy_shorties_media(url: str, request: Request):
    """Проксирует видео shorties, чтобы избежать ограничений hotlink/CORS."""
    parsed = urlparse(url)
    if parsed.scheme not in {"http", "https"} or not _is_allowed_shorties_media_host(parsed.hostname or ""):
        raise HTTPException(status_code=403, detail="Forbidden host")

    headers = {
        "User-Agent": (
            "Mozilla/5.0 (X11; Linux x86_64) "
            "AppleWebKit/537.36 (KHTML, like Gecko) "
            "Chrome/124.0.0.0 Safari/537.36"
        ),
        "Referer": "https://www.pornhub.com/shorties",
        "Origin": "https://www.pornhub.com",
    }
    range_header = request.headers.get("range")
    if range_header:
        headers["Range"] = range_header

    session = get_http_session()
    try:
        resp = await session.get(url, headers=headers, allow_redirects=True)
    except aiohttp.ClientError:
        raise HTTPException(status_code=502, detail="Failed to fetch media")

    if resp.status not in {200, 206}:
        await resp.release()
        raise HTTPException(status_code=resp.status, detail="Upstream error")

    content_type = resp.headers.get("Content-Type", "application/octet-stream")

    if ".m3u8" in parsed.path.lower() or "mpegurl" in content_type.lower():
        try:
            body = await resp.text()
        finally:
            await resp.release()

        def _proxy_media_url(target_url: str) -> str:
            return f"/api/proxy-shorties-media?url={quote(target_url, safe='')}"

        rewritten_lines: list[str] = []
        for raw_line in body.splitlines():
            stripped = raw_line.strip()
            if not stripped:
                rewritten_lines.append(raw_line)
                continue

            if stripped.startswith("#EXT-X-KEY") and 'URI="' in raw_line:
                prefix, rest = raw_line.split('URI="', 1)
                original_uri, suffix = rest.split('"', 1)
                absolute_uri = urljoin(url, original_uri)
                rewritten_lines.append(f'{prefix}URI="{_proxy_media_url(absolute_uri)}"{suffix}')
                continue

            if stripped.startswith("#"):
                rewritten_lines.append(raw_line)
                continue

            absolute_uri = urljoin(url, stripped)
            rewritten_lines.append(_proxy_media_url(absolute_uri))

        playlist_body = "\n".join(rewritten_lines)
        return Response(
            content=playlist_body,
            media_type=content_type,
            headers={
                "Cache-Control": "public, max-age=120",
                "Access-Control-Allow-Origin": "*",
            },
        )

    passthrough_headers = {
        "Cache-Control": "public, max-age=600",
        "Accept-Ranges": resp.headers.get("Accept-Ranges", "bytes"),
        "Access-Control-Allow-Origin": "*",
    }
    if resp.headers.get("Content-Length"):
        passthrough_headers["Content-Length"] = resp.headers["Content-Length"]
    if resp.headers.get("Content-Range"):
        passthrough_headers["Content-Range"] = resp.headers["Content-Range"]

    async def _stream():
        try:
            async for chunk in resp.content.iter_chunked(64 * 1024):
                yield chunk
        finally:
            await resp.release()

    return StreamingResponse(
        _stream(),
        media_type=content_type,
        status_code=resp.status,
        headers=passthrough_headers,
    )


# --- ИИ Чат ---

@app.post("/api/talk")
async def talk_api(
    req: TalkRequest,
    user: dict = Depends(get_current_user),
):
    """Отправить сообщение ИИ."""
    from AI.talk_handler import (
        BOT_MEMORY_NAME,
        EMPTY_RESPONSE_TEXT,
        SYSTEM_PROMPT,
        _generate_response,
        _normalize_reply,
        push_message,
    )

    user_id = user["user_id"]
    user_name = user["first_name"] or "кент"

    # Используем user_id как chat_id для личных сообщений в webapp
    chat_id = user_id

    try:
        await asyncio.to_thread(push_message, chat_id, "user", user_name, req.text, user_id=user_id)
        response = await _generate_response(
            chat_id,
            system_prompt=SYSTEM_PROMPT,
            user_id=user_id,
            max_tokens=220,
            temperature=0.8,
            top_p=0.9,
        )
        normalized_response = _normalize_reply(response)
        if not normalized_response:
            normalized_response = EMPTY_RESPONSE_TEXT
        await asyncio.to_thread(
            push_message,
            chat_id,
            "assistant",
            BOT_MEMORY_NAME,
            normalized_response,
            user_id=user_id,
        )
        return {"response": normalized_response}
    except Exception:
        logger.exception("Talk API failed")
        raise HTTPException(status_code=500, detail="LLM error")


# ──────────────────── Static files (для локального теста) ────────────────────

_frontend_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "frontend")
if os.path.isdir(_frontend_dir):
    app.mount("/", StaticFiles(directory=_frontend_dir, html=True), name="frontend")


# ──────────────────── Entry point ────────────────────

if __name__ == "__main__":
    import uvicorn
    port = int(os.getenv("API_PORT", "8080"))
    uvicorn.run(app, host="0.0.0.0", port=port)
