# -*- coding: utf-8 -*-
from datetime import timedelta
from typing import List
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from sqlmodel import Session

from app.core import security, config
from app.core.db import get_session
from app.crud import user as crud_user
import re
from jose import jwt
from app.crud import refresh_token as crud_refresh
from app.services.auth_service import (
    is_locked,
    record_failed_login,
    record_successful_login,
)
from fastapi import Request
from app.crud import session as session_crud
from app.api.deps import oauth2_scheme
from app.schemas.auth import Token, UserCreate, UserResponse, UserActivityResponse, ActivityCreate, GoogleLoginRequest, UserProfileUpdate, PasswordResetRequest, PasswordResetConfirm
from app.models.models import User
from app.api import deps
from app.core.limiter import limiter
from pydantic import BaseModel
from datetime import datetime, timedelta
import json
import logging
import secrets

logger = logging.getLogger(__name__)
router = APIRouter()

# Failed-login tracking and lockout handled by `app.services.auth_service` (SRP)

@router.post("/token", response_model=Token)
@limiter.limit("10/minute")
def login_for_access_token(request: Request, form_data: OAuth2PasswordRequestForm = Depends(), session: Session = Depends(get_session)):
    username = form_data.username

    # Check temporary lockout first
    if is_locked(username):
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="Account temporarily locked due to repeated failed login attempts",
        )

    user = crud_user.get_user_by_username(session, username)
    # Check DB-persisted lockout (survives worker/container restarts)
    if user and user.lockout_until and user.lockout_until > datetime.utcnow():
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="Account temporarily locked due to repeated failed login attempts",
        )
    if not user or not security.verify_password(form_data.password, user.password_hash):
        user_id = user.id if user else None
        result = record_failed_login(session, username, user_id)
        if result.get("locked"):
            raise HTTPException(
                status_code=status.HTTP_403_FORBIDDEN,
                detail="Account temporarily locked due to repeated failed login attempts",
            )
        # Otherwise standard 401
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Bearer"},
        )
    access_token_expires = timedelta(minutes=config.settings.ACCESS_TOKEN_EXPIRE_MINUTES)
    token, expires_at, jti = security.create_access_token(
        data={"sub": user.username}, expires_delta=access_token_expires
    )

    # Persist session record so token can be revoked — fatal if it fails
    try:
        session_crud.create_session(session, user.id, jti, expires_at)
    except Exception:
        logger.error("Failed to persist session for user %s - aborting login", user.username, exc_info=True)
        raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Login failed")

    # Create a rotating refresh token (opaque) and persist hashed
    try:
        refresh_value = security.generate_refresh_token_value()
        refresh_hash = security.hash_token(refresh_value)
        refresh_expires = datetime.utcnow() + timedelta(days=config.settings.REFRESH_TOKEN_EXPIRE_DAYS)
        crud_refresh.create_refresh_token(session, user.id, refresh_hash, refresh_expires, session_jti=jti)
    except Exception:
        logger.warning("Failed to create refresh token for user %s", user.username, exc_info=True)
        refresh_value = None

    # Successful login: clear failure counters and log success
    try:
        record_successful_login(session, user.id, username)
    except Exception:
        logger.warning("Failed to record successful login for user %s", user.username, exc_info=True)
    resp = {"access_token": token, "token_type": "bearer"}
    if refresh_value:
        resp["refresh_token"] = refresh_value
    return resp

@router.post("/users", response_model=UserResponse)
def create_user(user: UserCreate, session: Session = Depends(get_session)):
    db_user = crud_user.get_user_by_username(session, user.username)
    if db_user:
        raise HTTPException(status_code=400, detail="Username already registered")
    # Enforce password policy from settings
    pwd = user.password or ""
    if len(pwd) < config.settings.PASSWORD_MIN_LENGTH:
        raise HTTPException(status_code=400, detail=f"Password must be at least {config.settings.PASSWORD_MIN_LENGTH} characters")
    if config.settings.PASSWORD_REQUIRE_NUMBER and not re.search(r"\d", pwd):
        raise HTTPException(status_code=400, detail="Password must contain at least one digit")
    if config.settings.PASSWORD_REQUIRE_SPECIAL and not re.search(r"[^A-Za-z0-9]", pwd):
        raise HTTPException(status_code=400, detail="Password must contain at least one special character")
    if config.settings.PASSWORD_REQUIRE_UPPER and not re.search(r"[A-Z]", pwd):
        raise HTTPException(status_code=400, detail="Password must contain at least one uppercase letter")

    return crud_user.create_user(session, user)


@router.post("/logout", status_code=204)
def logout(current_user: User = Depends(deps.get_current_active_user), token: str = Depends(oauth2_scheme), session: Session = Depends(get_session)):
    """Revoke the current access token (logout)."""
    try:
        payload = jwt.decode(token, config.settings.SECRET_KEY, algorithms=[config.settings.ALGORITHM])
        jti = payload.get('jti')
        if jti:
            session_crud.revoke_session(session, jti)
            # Revoke any refresh tokens associated with this session
            try:
                crud_refresh.revoke_all_for_session(session, jti)
            except Exception:
                logger.warning("Failed to revoke refresh tokens during logout for jti %s", jti, exc_info=True)
    except Exception:
        pass
    return {}


@router.post("/admin/users/{user_id}/revoke_sessions", status_code=200)
def admin_revoke_user_sessions(user_id: int, current_user: User = Depends(deps.get_current_active_user), session: Session = Depends(get_session)):
    """Admin-only: revoke all sessions for a user. Restricted to username 'admin'."""
    if current_user.username != 'admin':
        raise HTTPException(status_code=403, detail="Not authorized")
    count = session_crud.revoke_all_sessions_for_user(session, user_id)
    # Also revoke refresh tokens for that user
    try:
        crud_refresh.revoke_all_for_user(session, user_id)
    except Exception:
        pass
    return {"revoked": count}


class RefreshRequest(BaseModel):
    refresh_token: str


@router.post("/token/refresh", response_model=Token)
def refresh_access_token(payload: RefreshRequest, session: Session = Depends(get_session)):
    token_value = payload.refresh_token
    token_hash = security.hash_token(token_value)
    rt = crud_refresh.get_active_by_hash(session, token_hash)
    if not rt:
        raise HTTPException(status_code=401, detail="Invalid refresh token")
    if rt.expires_at and rt.expires_at < datetime.utcnow():
        raise HTTPException(status_code=401, detail="Refresh token expired")

    # Load user
    user = session.get(User, rt.user_id)
    if not user or not user.is_active:
        raise HTTPException(status_code=401, detail="Invalid refresh token")

    # Create new access token using the session jti if present
    access_expires = timedelta(minutes=config.settings.ACCESS_TOKEN_EXPIRE_MINUTES)
    token, expires_at, jti = security.create_access_token(
        data={"sub": user.username}, expires_delta=access_expires, jti=rt.session_jti
    )

    # Rotate refresh token: create new one and revoke old
    new_refresh_value = security.generate_refresh_token_value()
    new_hash = security.hash_token(new_refresh_value)
    new_expires = datetime.utcnow() + timedelta(days=config.settings.REFRESH_TOKEN_EXPIRE_DAYS)
    new_rt = crud_refresh.create_refresh_token(session, user.id, new_hash, new_expires, session_jti=rt.session_jti)
    # revoke old and set replaced_by
    try:
        rt.revoked = True
        rt.replaced_by = new_rt.id
        session.add(rt)
        session.commit()
    except Exception:
        logger.warning("Failed to revoke old refresh token id=%s", rt.id, exc_info=True)

    return {"access_token": token, "token_type": "bearer", "refresh_token": new_refresh_value}

@router.get("/users/me", response_model=UserResponse)
def read_users_me(current_user: User = Depends(deps.get_current_active_user)):
    return current_user


@router.get("/users/{user_id}/public", response_model=UserResponse)
def read_user_public(user_id: int, session: Session = Depends(get_session)):
    """Public profile — strips sensitive fields at schema level."""
    user = session.get(User, user_id)
    if not user or not user.is_active:
        raise HTTPException(status_code=404, detail="User not found")
    return user


@router.patch("/users/me/profile", response_model=UserResponse)
def update_profile(
    payload: UserProfileUpdate,
    current_user: User = Depends(deps.get_current_active_user),
    session: Session = Depends(get_session),
):
    updated_fields = payload.model_dump(exclude_unset=True)
    for field, value in updated_fields.items():
        setattr(current_user, field, value)
    session.add(current_user)
    session.commit()
    session.refresh(current_user)
    return current_user

@router.get("/users/me/activity", response_model=List[UserActivityResponse])
def read_my_activity(
    current_user: User = Depends(deps.get_current_active_user),
    session: Session = Depends(get_session)
):
    return crud_user.get_user_activity(session, current_user.id)

@router.post("/activity", response_model=UserActivityResponse, status_code=201)
def create_activity(
    payload: ActivityCreate,
    current_user: User = Depends(deps.get_current_active_user),
    session: Session = Depends(get_session)
):
    return crud_user.log_activity(session, current_user.id, payload.action, payload.details)


@router.post("/auth/google", response_model=Token)
def login_with_google(payload: GoogleLoginRequest, session: Session = Depends(get_session)):
    """Exchange a Google OAuth2 access token for an app JWT + refresh token."""
    import requests as http_req

    if not config.settings.GOOGLE_CLIENT_ID:
        raise HTTPException(status_code=501, detail="Google login is not configured on this server")

    # Verify the access token via Google's tokeninfo endpoint and check audience
    try:
        resp = http_req.get(
            "https://www.googleapis.com/oauth2/v3/tokeninfo",
            params={"access_token": payload.access_token},
            timeout=5,
        )
        if not resp.ok:
            raise ValueError("Token rejected by Google")
        idinfo = resp.json()
        if idinfo.get("aud") != config.settings.GOOGLE_CLIENT_ID:
            raise ValueError("Token audience mismatch")
    except ValueError as exc:
        raise HTTPException(status_code=401, detail="Invalid Google token") from exc
    except Exception as exc:
        raise HTTPException(status_code=401, detail="Could not verify Google token") from exc

    google_id = idinfo.get("sub")
    email = idinfo.get("email", "")
    if not google_id:
        raise HTTPException(status_code=401, detail="Invalid Google token")

    # Fetch display name from userinfo endpoint
    display_name = email
    try:
        userinfo_resp = http_req.get(
            "https://www.googleapis.com/oauth2/v2/userinfo",
            headers={"Authorization": f"Bearer {payload.access_token}"},
            timeout=5,
        )
        if userinfo_resp.ok:
            display_name = userinfo_resp.json().get("name", "") or email
    except Exception:
        pass

    user = crud_user.get_or_create_google_user(session, google_id, email, display_name)

    if not user.is_active:
        raise HTTPException(status_code=400, detail="Account is inactive")

    access_token_expires = timedelta(minutes=config.settings.ACCESS_TOKEN_EXPIRE_MINUTES)
    token, expires_at, jti = security.create_access_token(
        data={"sub": user.username}, expires_delta=access_token_expires
    )

    try:
        session_crud.create_session(session, user.id, jti, expires_at)
    except Exception:
        logger.error("Failed to persist session for Google user %s - aborting login", user.username, exc_info=True)
        raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Login failed")

    refresh_value = None
    try:
        refresh_value = security.generate_refresh_token_value()
        refresh_hash = security.hash_token(refresh_value)
        refresh_expires = datetime.utcnow() + timedelta(days=config.settings.REFRESH_TOKEN_EXPIRE_DAYS)
        crud_refresh.create_refresh_token(session, user.id, refresh_hash, refresh_expires, session_jti=jti)
    except Exception:
        logger.warning("Failed to create refresh token for Google user %s", user.username, exc_info=True)
        refresh_value = None

    try:
        crud_user.log_activity(session, user.id, "login_google", json.dumps({"email": email}))
    except Exception:
        logger.warning("Failed to log Google login activity for user %s", user.username, exc_info=True)

    resp_data = {"access_token": token, "token_type": "bearer"}
    if refresh_value:
        resp_data["refresh_token"] = refresh_value
    return resp_data


@router.post("/password-reset/request", status_code=200)
def request_password_reset(payload: PasswordResetRequest, session: Session = Depends(get_session)):
    """Generate a password reset token for the account associated with the given email.
    Always returns 200 to prevent user enumeration.
    The token is logged server-side; wire up email delivery when SMTP is available.
    """
    user = crud_user.get_user_by_email(session, payload.email.lower().strip())
    if user and user.is_active and not user.google_id:
        token = secrets.token_urlsafe(32)
        user.reset_token = token
        user.reset_token_expires = datetime.utcnow() + timedelta(hours=1)
        session.add(user)
        session.commit()
        logger.info("Password reset requested for user '%s' — token: %s", user.username, token)
    return {"message": "If an account is registered with that email, a reset token has been generated."}


@router.post("/password-reset/confirm", status_code=200)
def confirm_password_reset(payload: PasswordResetConfirm, session: Session = Depends(get_session)):
    """Apply a password reset using the token issued by /password-reset/request."""
    user = crud_user.get_user_by_reset_token(session, payload.token)
    if not user or not user.reset_token_expires or user.reset_token_expires < datetime.utcnow():
        raise HTTPException(status_code=400, detail="Invalid or expired reset token")
    if len(payload.new_password) < config.settings.PASSWORD_MIN_LENGTH:
        raise HTTPException(status_code=400, detail=f"Password must be at least {config.settings.PASSWORD_MIN_LENGTH} characters")
    user.password_hash = security.get_password_hash(payload.new_password)
    user.reset_token = None
    user.reset_token_expires = None
    session.add(user)
    session.commit()
    # Revoke all active sessions so old tokens can't be reused
    try:
        session_crud.revoke_all_sessions_for_user(session, user.id)
    except Exception:
        pass
    logger.info("Password reset completed for user '%s'", user.username)
    return {"message": "Password updated successfully"}
