"""User self-service endpoints — change own username / password.

Only available to non-admin logged-in users (admins should manage their own
credentials through the admin panel / .env).
"""
from datetime import datetime, timedelta, timezone
from fastapi import APIRouter, HTTPException, Request
from pydantic import BaseModel, Field
from typing import Optional

from core.auth import (
    get_current_user,
    get_db,
    hash_password,
    verify_password,
    create_access_token,
)

router = APIRouter(prefix="/api/user", tags=["user-profile"])


class UpdateProfileIn(BaseModel):
    current_password: str = Field(min_length=1)
    new_username: Optional[str] = Field(default=None, min_length=3, max_length=32)
    new_password: Optional[str] = Field(default=None, min_length=4, max_length=128)


@router.put("/profile")
async def update_profile(body: UpdateProfileIn, request: Request):
    user = await get_current_user(request)
    if user.get("role") == "admin":
        raise HTTPException(
            status_code=403,
            detail="Admin credentials must be changed via .env / admin panel.",
        )

    db = get_db()
    full = await db.users.find_one({"id": user["id"]})
    if not full:
        raise HTTPException(status_code=401, detail="User not found")

    if not verify_password(body.current_password, full.get("password_hash", "")):
        raise HTTPException(status_code=401, detail="Current password is incorrect")

    updates: dict = {}

    new_username = (body.new_username or "").strip().lower()
    if new_username and new_username != full["username"]:
        taken = await db.users.find_one(
            {"username": new_username, "id": {"$ne": full["id"]}}
        )
        if taken:
            raise HTTPException(status_code=409, detail="Username already taken")
        updates["username"] = new_username

    if body.new_password:
        updates["password_hash"] = hash_password(body.new_password)

    if not updates:
        raise HTTPException(status_code=400, detail="Nothing to update")

    await db.users.update_one({"id": full["id"]}, {"$set": updates})

    refreshed = await db.users.find_one({"id": full["id"]}, {"_id": 0, "password_hash": 0})
    # Re-issue JWT if username changed (sub stays the same, but username claim must match)
    new_token = create_access_token(
        refreshed["id"], refreshed["username"], refreshed.get("role", "user")
    )
    return {"access_token": new_token, "token_type": "bearer", "user": refreshed}


# ── Rewarded-ads "watch N ads to unlock M hours ad-free" ──────────────
# BUG-5 FEATURE (2026-05): the React layer fires `POST /api/user/grant-
# ad-free` after the user finishes watching the configured number of
# rewarded videos (default 2). We stamp `user.ad_free_until` so that
# subsequent calls to `/api/auth/me` propagate the entitlement to every
# device the user signs into. Guests are handled client-side only.

class GrantAdFreeIn(BaseModel):
    hours: float = Field(default=2.0, gt=0, le=24, description="How many hours of ad-free to grant")


@router.post("/grant-ad-free")
async def grant_ad_free(body: GrantAdFreeIn, request: Request):
    """Stamp `ad_free_until = max(now, current) + hours` on the user's profile.

    The frontend calls this after the user has finished watching the
    required number of rewarded video ads. We always EXTEND the existing
    window rather than overwrite it, so back-to-back rewards stack
    instead of resetting.
    """
    user = await get_current_user(request)
    if user.get("role") == "admin":
        # Admins never see ads, so granting them ad-free is a no-op —
        # return their current state so the client can render correctly.
        db = get_db()
        full = await db.users.find_one({"id": user["id"]}, {"_id": 0, "password_hash": 0})
        return {"ad_free_until": full.get("ad_free_until"), "granted_hours": 0.0}

    db = get_db()
    full = await db.users.find_one({"id": user["id"]})
    if not full:
        raise HTTPException(status_code=401, detail="User not found")

    now = datetime.now(timezone.utc)
    base_iso = full.get("ad_free_until")
    base: datetime
    if base_iso:
        try:
            base = datetime.fromisoformat(base_iso)
            if base.tzinfo is None:
                base = base.replace(tzinfo=timezone.utc)
        except Exception:
            base = now
    else:
        base = now
    if base < now:
        base = now

    new_until = base + timedelta(hours=float(body.hours))
    await db.users.update_one(
        {"id": full["id"]}, {"$set": {"ad_free_until": new_until.isoformat()}}
    )
    return {
        "ad_free_until": new_until.isoformat(),
        "granted_hours": float(body.hours),
        "seconds_remaining": max(0, int((new_until - now).total_seconds())),
    }


@router.get("/ad-free-status")
async def ad_free_status(request: Request):
    """Lightweight read of the current user's ad-free window. Used by the
    React layer to refresh the entitlement after a reward without
    re-fetching the whole `/auth/me` payload."""
    user = await get_current_user(request)
    db = get_db()
    full = await db.users.find_one({"id": user["id"]}, {"_id": 0, "password_hash": 0})
    if not full:
        raise HTTPException(status_code=401, detail="User not found")
    until_iso = full.get("ad_free_until")
    now = datetime.now(timezone.utc)
    seconds_remaining = 0
    if until_iso:
        try:
            until = datetime.fromisoformat(until_iso)
            if until.tzinfo is None:
                until = until.replace(tzinfo=timezone.utc)
            seconds_remaining = max(0, int((until - now).total_seconds()))
        except Exception:
            seconds_remaining = 0
    return {
        "ad_free_until": until_iso,
        "seconds_remaining": seconds_remaining,
        "active": seconds_remaining > 0,
    }
