from fastapi import Depends, HTTPException, status, Request, Cookie
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from jose import JWTError, jwt
from sqlalchemy.orm import Session, joinedload
from typing import Optional
from datetime import datetime
import os
from ...shared.config.database import get_db
from ...shared.config.settings import settings
from ...auth.models.user import User
from ...auth.models.role import Role
security = HTTPBearer(auto_error=False)

def get_jwt_secret() -> str:
    """
    Get JWT secret securely, fail if not configured.
    Never use hardcoded fallback secrets.
    """
    # Remove default secret entirely - fail fast if not configured
    jwt_secret = getattr(settings, 'JWT_SECRET', None) or os.getenv('JWT_SECRET', None)
    
    # Fail fast if secret is not configured
    if not jwt_secret:
        error_msg = (
            'CRITICAL: JWT_SECRET is not configured. '
            'Please set JWT_SECRET environment variable to a secure random string (minimum 32 characters).'
        )
        import logging
        logger = logging.getLogger(__name__)
        logger.error(error_msg)
        if settings.is_production:
            raise ValueError(error_msg)
        else:
            # In development, generate a secure secret but warn
            import secrets
            jwt_secret = secrets.token_urlsafe(64)
            logger.warning(
                f'JWT_SECRET not configured. Auto-generated secret for development. '
                f'Set JWT_SECRET environment variable for production: {jwt_secret}'
            )
    
    # Validate JWT secret strength
    if len(jwt_secret) < 32:
        error_msg = 'JWT_SECRET must be at least 32 characters long for security.'
        import logging
        logger = logging.getLogger(__name__)
        logger.error(error_msg)
        if settings.is_production:
            raise ValueError(error_msg)
        else:
            logger.warning(error_msg)
    
    # SECURITY: Validate JWT secret entropy (check for predictable patterns)
    # Check if secret appears to be randomly generated (not a simple pattern)
    if len(set(jwt_secret)) < len(jwt_secret) * 0.3:  # Less than 30% unique characters suggests low entropy
        error_msg = 'JWT_SECRET appears to have low entropy. Please use a randomly generated secret.'
        import logging
        logger = logging.getLogger(__name__)
        logger.error(error_msg)
        if settings.is_production:
            raise ValueError(error_msg)
        else:
            logger.warning(error_msg)
    
    return jwt_secret

def get_current_user(
    request: Request,
    credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
    access_token: Optional[str] = Cookie(None, alias='accessToken'),
    db: Session = Depends(get_db)
) -> User:
    """
    Get current user from either Authorization header or httpOnly cookie.
    Prefers Authorization header for backward compatibility, falls back to cookie.
    """
    # Try to get token from Authorization header first
    token = None
    if credentials:
        token = credentials.credentials
    
    # Fall back to cookie if no header token
    if not token and access_token:
        token = access_token
    
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail='Could not validate credentials',
        headers={'WWW-Authenticate': 'Bearer'}
    )
    
    if not token:
        raise credentials_exception
    
    try:
        jwt_secret = get_jwt_secret()
        payload = jwt.decode(token, jwt_secret, algorithms=['HS256'])
        user_id: int = payload.get('userId')
        if user_id is None:
            raise credentials_exception
    except JWTError:
        raise credentials_exception
    except ValueError as e:
        # JWT secret configuration error - should not happen in production
        raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail='Server configuration error')
    # PERFORMANCE: Eager load role relationship to avoid N+1 queries in authorize_roles
    user = db.query(User).options(joinedload(User.role)).filter(User.id == user_id).first()
    if user is None:
        raise credentials_exception
    
    # Check if user account is active
    if not user.is_active:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail='Account is disabled. Please contact support.'
        )
    
    # Check if account is locked
    if user.locked_until and user.locked_until > datetime.utcnow():
        remaining_minutes = int((user.locked_until - datetime.utcnow()).total_seconds() / 60)
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail=f'Account is temporarily locked due to multiple failed login attempts. Please try again in {remaining_minutes} minute(s).'
        )
    
    # SECURITY: Check MFA for accountant roles (warn but allow access for MFA setup)
    # Note: MFA enforcement for financial endpoints is handled by route-level dependencies
    # This check only logs warnings to allow users to access MFA setup pages
    try:
        from ...payments.services.accountant_security_service import accountant_security_service
        from ...shared.utils.role_helpers import is_accountant, is_admin
        from ...shared.config.logging_config import get_logger
        
        logger = get_logger(__name__)
        
        if is_accountant(user, db):
            is_enforced, reason = accountant_security_service.is_mfa_enforced(user, db)
            if not is_enforced and reason:
                # Log warning but allow access so user can set up MFA
                # Individual routes enforce MFA for sensitive operations using enforce_mfa_for_accountants()
                logger.warning(
                    f'User {user.id} ({user.email}) accessed system without MFA enabled. '
                    f'MFA is required for {user.role.name if user.role else "unknown"} role. '
                    f'Reason: {reason}'
                )
                # Store MFA requirement in user object for route-level checks
                user._mfa_setup_required = True
                user._mfa_setup_reason = reason
    except HTTPException:
        raise
    except Exception as e:
        # Don't block authentication if MFA check fails (log and continue)
        logger = get_logger(__name__)
        logger.warning(f'Error checking MFA enforcement: {str(e)}')
    
    return user

def authorize_roles(*allowed_roles: str):

    def role_checker(current_user: User=Depends(get_current_user), db: Session=Depends(get_db)) -> User:
        # PERFORMANCE: Use eager-loaded relationship if available, otherwise query
        # This reduces database queries since get_current_user now eager loads the role
        from ...shared.utils.role_helpers import get_user_role_name

        if hasattr(current_user, 'role') and current_user.role is not None:
            user_role_name = current_user.role.name
        else:
            # Fallback: query role if relationship wasn't loaded (shouldn't happen, but safe)
            role = db.query(Role).filter(Role.id == current_user.role_id).first()
            if not role:
                raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail='User role not found')
            user_role_name = role.name
        
        # Backwards‑compatible support for accountant sub‑roles:
        # any role starting with "accountant_" is treated as "accountant"
        # when a route allows plain "accountant".
        if user_role_name not in allowed_roles:
            # Normalise accountant_* → accountant for role checks
            if (
                user_role_name.startswith("accountant_")
                and "accountant" in allowed_roles
            ):
                # treated as allowed
                return current_user
            raise HTTPException(
                status_code=status.HTTP_403_FORBIDDEN,
                detail='You do not have permission to access this resource'
            )
        return current_user
    return role_checker

def get_current_user_optional(
    request: Request,
    credentials: Optional[HTTPAuthorizationCredentials] = Depends(HTTPBearer(auto_error=False)),
    access_token: Optional[str] = Cookie(None, alias='accessToken'),
    db: Session = Depends(get_db)
) -> Optional[User]:
    """
    Get current user optionally from either Authorization header or httpOnly cookie.
    Returns None if no valid token is found, or if user is inactive/locked.
    This ensures inactive/locked users are never considered "authenticated" even for optional features.
    """
    # Try to get token from Authorization header first
    token = None
    if credentials:
        token = credentials.credentials
    
    # Fall back to cookie if no header token
    if not token and access_token:
        token = access_token
    
    if not token:
        return None
    
    try:
        jwt_secret = get_jwt_secret()
        payload = jwt.decode(token, jwt_secret, algorithms=['HS256'])
        user_id: int = payload.get('userId')
        if user_id is None:
            return None
    except (JWTError, ValueError):
        return None
    
    # PERFORMANCE: Eager load role relationship for consistency
    user = db.query(User).options(joinedload(User.role)).filter(User.id == user_id).first()
    if user is None:
        return None
    
    # Check if user account is active - return None for inactive users
    if not user.is_active:
        return None
    
    # Check if account is locked - return None for locked users
    if user.locked_until and user.locked_until > datetime.utcnow():
        return None
    
    return user

def verify_token(token: str) -> dict:
    jwt_secret = get_jwt_secret()
    payload = jwt.decode(token, jwt_secret, algorithms=['HS256'])
    return payload