"""
Session-based auth: login, logout, me.
Email verification only for registration (invite/set-password). No OTP for login.
"""
import os
import secrets
import random
import string
from datetime import datetime, timedelta
from typing import Optional, Tuple
from fastapi import APIRouter, Depends, HTTPException, Request, Response
from fastapi import status
from fastapi.responses import JSONResponse
from pydantic import BaseModel
from sqlalchemy.orm import Session
from passlib.context import CryptContext

from app.database import get_db
from app.models import User, Role, Permission, Session as SessionModel
from app.services.email_service import send_invite_email, send_otp_email, send_reset_password_email

pwd_ctx = CryptContext(schemes=["bcrypt"], deprecated="auto")

SESSION_COOKIE_NAME = "session_token"
SESSION_DAYS_DEFAULT = 7
SESSION_DAYS_REMEMBER = 30
OTP_EXPIRE_MINUTES = 10  # used by request-otp (e.g. for future registration verification)

router = APIRouter(prefix="/api/auth", tags=["auth"])


class LoginBody(BaseModel):
    username: str
    password: str
    remember_me: bool = False


class SetPasswordBody(BaseModel):
    token: str
    password: str


class RequestPasswordResetBody(BaseModel):
    email: str


class ResetPasswordBody(BaseModel):
    token: str
    password: str


class VerifyOtpBody(BaseModel):
    code: str


def _session_days(remember_me: bool) -> int:
    return int(os.getenv("SESSION_REMEMBER_DAYS", str(SESSION_DAYS_REMEMBER))) if remember_me else int(os.getenv("SESSION_MAX_DAYS", str(SESSION_DAYS_DEFAULT)))


def _cookie_max_age(remember_me: bool) -> int:
    return _session_days(remember_me) * 24 * 60 * 60


def _secure_cookie() -> bool:
    """Use Secure cookie when SECURE_COOKIES is set (e.g. production HTTPS)."""
    return os.getenv("SECURE_COOKIES", "false").lower() in ("1", "true", "yes")


@router.post("/login")
async def login(
    body: LoginBody,
    response: Response,
    db: Session = Depends(get_db),
):
    """Verify username/password, create session, set httpOnly cookie."""
    user = db.query(User).filter(User.username == body.username).first()
    if not user or not pwd_ctx.verify(body.password, user.password_hash):
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid username or password")
    if getattr(user, "invite_token", None):
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="Please set your password using the link from your invite email first.",
        )

    days = _session_days(body.remember_me)
    expires_at = datetime.utcnow() + timedelta(days=days)
    token = secrets.token_urlsafe(32)
    now = datetime.utcnow()
    session = SessionModel(
        user_id=user.id,
        token=token,
        expires_at=expires_at,
        remember_me=body.remember_me,
        otp_verified_at=now,
    )
    db.add(session)
    db.commit()

    effective_role = get_effective_role(db, user)
    permissions = get_user_permissions(db, user)
    role = _get_role_for_user(db, user)
    resp = JSONResponse(content={
        "user": {
            "id": user.id,
            "username": user.username,
            "role": effective_role,
            "role_id": user.role_id,
            "role_name": role.name if role else effective_role.capitalize(),
            "permissions": permissions,
        }
    })
    resp.set_cookie(
        key=SESSION_COOKIE_NAME,
        value=token,
        max_age=_cookie_max_age(body.remember_me),
        httponly=True,
        samesite="strict",
        secure=_secure_cookie(),
        path="/",
    )
    return resp


@router.post("/logout")
async def logout(response: Response, request: Request, db: Session = Depends(get_db)):
    """Invalidate session and clear cookie."""
    token = request.cookies.get(SESSION_COOKIE_NAME)
    if token:
        db.query(SessionModel).filter(SessionModel.token == token).delete()
        db.commit()
    resp = JSONResponse(content={"ok": True})
    resp.delete_cookie(SESSION_COOKIE_NAME, path="/")
    return resp


# ---------- Invite: set password (public) ----------


@router.get("/invite")
async def get_invite_info(token: str, db: Session = Depends(get_db)):
    """Public: return invite details for set-password page. Valid if token exists and not expired."""
    user = db.query(User).filter(User.invite_token == token).first()
    if not user or not user.invite_expires_at or user.invite_expires_at < datetime.utcnow():
        return {"valid": False, "email": None, "role_name": None}
    role = _get_role_for_user(db, user)
    return {
        "valid": True,
        "email": user.username,
        "role_name": role.name if role else (user.role or "User"),
    }


@router.post("/set-password")
async def set_password(body: SetPasswordBody, db: Session = Depends(get_db)):
    """Public: set password using invite token; clears invite state so user can log in."""
    user = db.query(User).filter(User.invite_token == body.token).first()
    if not user or not user.invite_expires_at or user.invite_expires_at < datetime.utcnow():
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid or expired invite link")
    if len(body.password) < 8:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Password must be at least 8 characters")
    user.password_hash = pwd_ctx.hash(body.password)
    user.invite_token = None
    user.invite_expires_at = None
    db.commit()
    return {"ok": True, "message": "Password set. You can now log in."}


# ---------- Forgot password ----------


@router.post("/request-password-reset")
async def request_password_reset(body: RequestPasswordResetBody, db: Session = Depends(get_db)):
    """Public: send password reset email. Always returns ok to prevent email enumeration."""
    email = (body.email or "").strip().lower()
    if not email or "@" not in email:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Valid email required")
    user = db.query(User).filter(User.username == email).first()
    if user and not getattr(user, "invite_token", None):
        # User exists and is not pending invite - can reset
        reset_token = secrets.token_urlsafe(32)
        reset_expires_at = datetime.utcnow() + timedelta(hours=1)
        user.reset_token = reset_token
        user.reset_expires_at = reset_expires_at
        db.commit()
        app_url = (os.getenv("APP_URL") or os.getenv("FRONTEND_URL") or "").strip()
        if not app_url:
            app_url = "https://nixonstats.info" if (os.path.exists("/.dockerenv") or os.getenv("RUNNING_IN_DOCKER", "").lower() in ("1", "true", "yes")) else "http://localhost:3000"
        app_url = app_url.rstrip("/")
        reset_link = f"{app_url}/reset-password?token={reset_token}"
        send_reset_password_email(email, reset_link)
    return {"ok": True, "message": "If an account exists with that email, a reset link has been sent."}


@router.get("/reset-password-info")
async def get_reset_password_info(token: str, db: Session = Depends(get_db)):
    """Public: return reset token validity for reset-password page."""
    user = db.query(User).filter(User.reset_token == token).first()
    if not user or not getattr(user, "reset_expires_at", None) or user.reset_expires_at < datetime.utcnow():
        return {"valid": False, "email": None}
    return {"valid": True, "email": user.username}


@router.post("/reset-password")
async def reset_password(body: ResetPasswordBody, db: Session = Depends(get_db)):
    """Public: reset password using token from forgot-password email."""
    user = db.query(User).filter(User.reset_token == body.token).first()
    if not user or not getattr(user, "reset_expires_at", None) or user.reset_expires_at < datetime.utcnow():
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid or expired reset link")
    if len(body.password) < 8:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Password must be at least 8 characters")
    user.password_hash = pwd_ctx.hash(body.password)
    user.reset_token = None
    user.reset_expires_at = None
    db.commit()
    return {"ok": True, "message": "Password updated. You can now log in."}


# ---------- OTP re-verification (every 7 days) ----------


def _generate_otp_code() -> str:
    return "".join(random.choices(string.digits, k=6))


@router.post("/request-otp")
async def request_otp(request: Request, db: Session = Depends(get_db)):
    """Send OTP to session user's email. Call when /me returns otp_required."""
    session, user = _get_session_for_otp(request, db)
    code = _generate_otp_code()
    session.otp_code_hash = pwd_ctx.hash(code)
    session.otp_expires_at = datetime.utcnow() + timedelta(minutes=OTP_EXPIRE_MINUTES)
    db.commit()
    sent = send_otp_email(user.username, code)
    return {"ok": True, "sent": sent, "message": "Verification code sent to your email." if sent else "Code generated but email not sent (check SendGrid)."}


@router.post("/verify-otp")
async def verify_otp(body: VerifyOtpBody, request: Request, db: Session = Depends(get_db)):
    """Verify OTP code and refresh otp_verified_at so session is fully valid again."""
    session, user = _get_session_for_otp(request, db)
    if not session.otp_code_hash or not session.otp_expires_at or session.otp_expires_at < datetime.utcnow():
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Code expired. Request a new one.")
    if not pwd_ctx.verify(body.code, session.otp_code_hash):
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid code")
    session.otp_verified_at = datetime.utcnow()
    session.otp_code_hash = None
    session.otp_expires_at = None
    db.commit()
    effective_role = get_effective_role(db, user)
    permissions = get_user_permissions(db, user)
    role = _get_role_for_user(db, user)
    return {
        "user": {
            "id": user.id,
            "username": user.username,
            "role": effective_role,
            "role_id": user.role_id,
            "role_name": role.name if role else effective_role.capitalize(),
            "permissions": permissions,
        }
    }


def _get_session_for_otp(request: Request, db: Session) -> Tuple[SessionModel, User]:
    """Return session and user for valid cookie. Used by request-otp and verify-otp (no OTP check)."""
    session, user = _get_session_and_user(request, db)
    if not session or not user:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Session expired or invalid")
    return session, user


def _get_session_and_user(request: Request, db: Session) -> Tuple[Optional[SessionModel], Optional[User]]:
    """Return (session, user) for valid cookie, or (None, None). Does not check OTP."""
    token = request.cookies.get(SESSION_COOKIE_NAME)
    if not token:
        return None, None
    session = db.query(SessionModel).filter(SessionModel.token == token).first()
    if not session or session.expires_at < datetime.utcnow():
        if session:
            db.delete(session)
            db.commit()
        return None, None
    user = db.query(User).filter(User.id == session.user_id).first()
    if not user:
        return None, None
    return session, user


def get_current_user(
    request: Request,
    db: Session = Depends(get_db),
) -> User:
    """Dependency: require valid session cookie; return User or raise 401. No OTP re-verification for login."""
    session, user = _get_session_and_user(request, db)
    if not session or not user:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Not authenticated")
    return user


def _get_role_for_user(db: Session, user: User) -> Optional[Role]:
    """Resolve user's role from role_id or legacy role string."""
    if user.role_id:
        return db.query(Role).filter(Role.id == user.role_id).first()
    slug = (user.role or "user").lower()
    return db.query(Role).filter(Role.slug == slug).first()


def get_user_permissions(db: Session, user: User) -> list[str]:
    """Return list of permission keys for the user. Uses direct user permissions; falls back to role permissions if none set."""
    from app.models import user_permissions
    # Direct permissions from user_permissions table
    direct = (
        db.query(Permission.key)
        .join(user_permissions, user_permissions.c.permission_id == Permission.id)
        .filter(user_permissions.c.user_id == user.id)
        .all()
    )
    keys = [r[0] for r in direct]
    if keys:
        return keys
    # Backward compat: no direct permissions → use role permissions
    role = _get_role_for_user(db, user)
    if not role:
        return []
    return [p.key for p in role.permissions]


def get_effective_role(db: Session, user: User) -> str:
    """Return effective role slug (user, admin, director) or legacy user.role."""
    role = _get_role_for_user(db, user)
    if role and role.slug:
        return role.slug
    return user.role or "user"


def require_admin_role(user: User = Depends(get_current_user)) -> User:
    """Dependency: require current user to be admin or director."""
    if user.role not in ("admin", "director"):
        raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin or Director role required")
    return user


def require_director(
    user: User = Depends(get_current_user),
    db: Session = Depends(get_db),
) -> User:
    """Dependency: require current user to be director (by role or role_id)."""
    role = _get_role_for_user(db, user)
    if role and role.slug == "director":
        return user
    if user.role == "director":
        return user
    raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Director role required")


def require_permission(
    permission_key: str,
):
    """Dependency factory: require current user to have the given permission."""

    def _require(user: User = Depends(get_current_user), db: Session = Depends(get_db)):
        perms = get_user_permissions(db, user)
        if permission_key in perms:
            return user
        raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=f"Permission required: {permission_key}")

    return _require


@router.get("/me")
async def me(
    user: User = Depends(get_current_user),
    db: Session = Depends(get_db),
):
    """Return current user with role and permissions for UI gating."""
    effective_role = get_effective_role(db, user)
    permissions = get_user_permissions(db, user)
    role = _get_role_for_user(db, user)
    return {
        "user": {
            "id": user.id,
            "username": user.username,
            "role": effective_role,
            "role_id": user.role_id,
            "role_name": role.name if role else (effective_role.capitalize()),
            "permissions": permissions,
        }
    }
