"""Authentication router: login, logout and current-user endpoints.

The JWT access token is delivered as an **HttpOnly** cookie
(``aegis_token``) so it is inaccessible to client-side JavaScript,
mitigating XSS token-theft attacks.  The JSON response also includes
the token in the body for backwards compatibility and for clients that
cannot use cookies (e.g. Swagger UI).
"""

# Import os
import os

# Import APIRouter, Cookie, Depends, Request, Response from fastapi
from fastapi import APIRouter, Cookie, Depends, Request, Response

# Import OAuth2PasswordRequestForm from fastapi.security
from fastapi.security import OAuth2PasswordRequestForm

# Import datetime, timezone from datetime
from datetime import datetime, timezone

# Import jwt (PyJWT)
import jwt
from jwt.exceptions import PyJWTError as JWTError

# Import Session from sqlalchemy.orm
from sqlalchemy.orm import Session

# Import blacklist_token, create_access_token, verify_pa... from app.auth
from app.auth import blacklist_token, create_access_token, is_token_blacklisted, verify_password

# Import settings from app.config
from app.config import settings

# Import get_db from app.database
from app.database import get_db

# Import get_current_user from app.dependencies.auth
from app.dependencies.auth import get_current_user

# Import BusinessRuleViolation, PermissionViolation from app.domain.errors
from app.domain.errors import BusinessRuleViolation, PermissionViolation

# Import UnitOfWork from app.domain.unit_of_work
from app.domain.unit_of_work import UnitOfWork

# Import limiter from app.limiter
from app.limiter import limiter

# Import resolve_client_ip from app.middleware.request_context
from app.middleware.request_context import resolve_client_ip

# Import User from app.models.user
from app.models.user import User

# Import TokenResponse, UserOut from app.schemas.auth
from app.schemas.auth import TokenResponse, UserOut

# Import PasswordChange from app.schemas.user
from app.schemas.user import PasswordChange

# Import log_action from app.services.audit_service
from app.services.audit_service import log_action

# Import  from app.services.auth_service
from app.services.auth_service import (
    _DUMMY_HASH,
)

# Import  from app.services.auth_service
from app.services.auth_service import (
    change_password as auth_change_password,
)

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

# SECURE_COOKIES desacopla la seguridad de la cookie del entorno de ejecucion.
# Por defecto activo en produccion; ponlo en "false" para servidores HTTP.
_aegis_env = os.environ.get("AEGIS_ENV", "development").lower()
_secure_cookie_env = os.environ.get("SECURE_COOKIES", "auto").lower()
if _secure_cookie_env == "false":
    _IS_HTTPS = False
elif _secure_cookie_env == "true":
    _IS_HTTPS = True
else:  # "auto" — activo solo si AEGIS_ENV=production
    _IS_HTTPS = _aegis_env == "production"
_COOKIE_NAME = "aegis_token"


# Apply the @router.post decorator
@router.post("/login", response_model=TokenResponse)
# Apply the @limiter.limit decorator
@limiter.limit("5/minute")
# Define function login
def login(
    # Entry: request
    request: Request,
    # Entry: response
    response: Response,
    # Entry: form_data
    form_data: OAuth2PasswordRequestForm = Depends(),
    # Entry: db
    db: Session = Depends(get_db),
) -> TokenResponse:
    """Authenticate a user and return a JWT access token.

    Rate-limited to **5 attempts per minute per IP**.  Failed and successful
    logins are recorded in the audit log (SEC-009).
    """
    # Assign user = db.query(User).filter(User.username == form_data.username).first()
    user = db.query(User).filter(User.username == form_data.username).first()
    # Assign target_hash = user.hashed_password if user else _DUMMY_HASH
    target_hash = user.hashed_password if user else _DUMMY_HASH
    # Assign password_valid = verify_password(form_data.password, target_hash)
    password_valid = verify_password(form_data.password, target_hash)
    # Assign ip = resolve_client_ip(request)
    ip = resolve_client_ip(request)

    # Check: user is None or not password_valid
    if user is None or not password_valid:
        # Open context manager
        with UnitOfWork(db) as uow:
            # Call log_action()
            log_action(
                db,
                user.id if user else None,
                # Literal argument value
                "LOGIN_FAILED",
                # Literal argument value
                "auth",
                # Literal argument value
                None,
                # Keyword argument: details
                details={
                    # Literal argument value
                    "username": form_data.username,
                    # Literal argument value
                    "ip": ip,
                    # Literal argument value
                    "reason": "invalid_credentials",
                },
                # Keyword argument: ip_address
                ip_address=ip,
            )
            # Call uow.commit()
            uow.commit()
        # Raise BusinessRuleViolation
        raise BusinessRuleViolation("Incorrect username or password")

    # Check: not user.is_active
    if not user.is_active:
        # Raise PermissionViolation
        raise PermissionViolation("Account is disabled. Contact an administrator.")

    # Assign access_token = create_access_token(data={"sub": user.username})
    access_token = create_access_token(data={"sub": user.username})

    # Open context manager
    with UnitOfWork(db) as uow:
        # Call log_action()
        log_action(
            db,
            user.id,
            # Literal argument value
            "LOGIN_SUCCESS",
            # Literal argument value
            "auth",
            str(user.id),
            # Keyword argument: details
            details={"username": user.username, "ip": ip},
            # Keyword argument: ip_address
            ip_address=ip,
        )
        # Call uow.commit()
        uow.commit()

    # Auto-discover the user's Jira account ID on every login (non-fatal)
    try:
        from app.services.jira_service import lookup_user_jira_account_id
        if lookup_user_jira_account_id(db, user):
            db.commit()
    except Exception:
        pass

    # Call response.set_cookie()
    response.set_cookie(
        # Keyword argument: key
        key=_COOKIE_NAME,
        # Keyword argument: value
        value=access_token,
        # Keyword argument: httponly
        httponly=True,
        # Keyword argument: secure
        secure=_IS_HTTPS,
        # Keyword argument: samesite
        samesite="strict",
        # Keyword argument: max_age
        max_age=settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60,
        # Keyword argument: path
        path="/",
    )

    # Return TokenResponse(access_token=access_token)
    return TokenResponse(access_token=access_token)


# Apply the @router.post decorator
@router.post("/logout")
# Define function logout
def logout(
    # Entry: request
    request: Request,
    # Entry: response
    response: Response,
    # Entry: aegis_token
    aegis_token: str | None = Cookie(None),
) -> dict:
    """Clear the authentication cookie and revoke the current token."""
    # Assign bearer = (
    bearer = (
        request.headers.get("Authorization")
        or request.headers.get("authorization")
        or ""
    )
    # Assign bearer = bearer.removeprefix("Bearer ").removeprefix("bearer ").strip()
    bearer = bearer.removeprefix("Bearer ").removeprefix("bearer ").strip()

    # Assign seen = set()
    seen: set[str] = set()
    # Iterate over (aegis_token, bearer)
    for raw in (aegis_token, bearer):
        # Check: not raw or raw in seen
        if not raw or raw in seen:
            # Skip to the next loop iteration
            continue
        # Call seen.add()
        seen.add(raw)
        # Attempt the following; catch errors below
        try:
            # Assign payload = jwt.decode(
            payload = jwt.decode(
                raw,
                settings.SECRET_KEY,
                # Keyword argument: algorithms
                algorithms=[settings.ALGORITHM],
            )
            # Assign jti = payload.get("jti")
            jti = payload.get("jti")
            # Assign exp = payload.get("exp", 0)
            exp = payload.get("exp", 0)
            # Check: jti
            if jti:
                # Call blacklist_token()
                blacklist_token(jti, float(exp))
        # Handle any JWT validation error during logout (token may be expired or malformed)
        except jwt.exceptions.InvalidTokenError:
            # Intentional no-op placeholder
            pass

    # Call response.delete_cookie()
    response.delete_cookie(
        # Keyword argument: key
        key=_COOKIE_NAME,
        # Keyword argument: httponly
        httponly=True,
        # Keyword argument: secure
        secure=_IS_HTTPS,
        # Keyword argument: samesite
        samesite="strict",
        # Keyword argument: path
        path="/",
    )
    # Return {"detail": "Logged out"}
    return {"detail": "Logged out"}


# A token that has *just* expired can still be refreshed within this window.
# Without this grace period, `/auth/refresh` decodes the same token with the
# same strict expiry check as every other endpoint — so by the time a 401
# triggers a refresh attempt, the refresh call itself has also expired and
# the session is unrecoverable. The grace window only widens the refresh
# check; a brand-new token is always issued with the full expiry.
_REFRESH_GRACE_SECONDS = 15 * 60


@router.post("/refresh", response_model=TokenResponse)
def refresh_token(
    response: Response,
    aegis_token: str | None = Cookie(None),
    db: Session = Depends(get_db),
):
    """Issue a new access token if the current one is valid or recently expired.

    Called automatically by the frontend when it detects an expired
    session while the user is actively using the app. Expiry is checked
    manually (with `_REFRESH_GRACE_SECONDS` of leeway) instead of relying on
    PyJWT's built-in check, so a token that expired moments ago can still
    be refreshed. Revoked (blacklisted) tokens are never refreshable.
    """
    if not aegis_token:
        raise PermissionViolation("No active session")

    try:
        payload = jwt.decode(
            aegis_token,
            settings.SECRET_KEY,
            algorithms=[settings.ALGORITHM],
            options={"verify_exp": False},
        )
    except JWTError:
        raise PermissionViolation("Session expired — please log in again")

    jti: str | None = payload.get("jti")
    if jti and is_token_blacklisted(jti):
        raise PermissionViolation("Session expired — please log in again")

    exp = payload.get("exp")
    now = datetime.now(timezone.utc).timestamp()
    if exp is None or now - exp > _REFRESH_GRACE_SECONDS:
        raise PermissionViolation("Session expired — please log in again")

    username: str | None = payload.get("sub")
    if not username:
        raise PermissionViolation("Invalid session")

    user = db.query(User).filter(User.username == username).first()
    if user is None or not user.is_active:
        raise PermissionViolation("Account not found or disabled")

    if getattr(user, "must_change_password", False):
        raise PermissionViolation("Password change required before refreshing session")

    # Issue a fresh token with a new expiry
    new_token = create_access_token(data={"sub": user.username})
    response.set_cookie(
        key=_COOKIE_NAME,
        value=new_token,
        httponly=True,
        secure=_IS_HTTPS,
        samesite="strict",
        max_age=settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60,
        path="/",
    )
    return TokenResponse(access_token=new_token)


@router.get("/me", response_model=UserOut)
# Define function read_current_user
def read_current_user(current_user: User = Depends(get_current_user)) -> UserOut:
    """Return the profile of the currently authenticated user."""
    # Return current_user
    return current_user


# Apply the @router.post decorator
@router.post("/change-password")
# Define function change_password
def change_password(
    # Entry: body
    body: PasswordChange,
    # Entry: db
    db: Session = Depends(get_db),
    # Entry: current_user
    current_user: User = Depends(get_current_user),
) -> dict:
    """Change the current user's password."""
    # Call auth_change_password()
    auth_change_password(
        db,
        current_user,
        # Keyword argument: current_password
        current_password=body.current_password,
        # Keyword argument: new_password
        new_password=body.new_password,
    )
    # Open context manager
    with UnitOfWork(db) as uow:
        # Call uow.commit()
        uow.commit()

    # Return {"detail": "Password changed successfully"}
    return {"detail": "Password changed successfully"}
