from typing import Optional, Dict, Any
from sqlalchemy.orm import Session
import logging

from genovation_advanced_auth.models.user import User
from genovation_advanced_auth.core.mfa import MFAManager
from genovation_advanced_auth.core.security import (
    hash_backup_codes,
    verify_backup_code,
    verify_password,
    generate_backup_codes
)
from genovation_advanced_auth.core.config import AuthConfig, get_config
from genovation_advanced_auth.exceptions import (
    InvalidMFACodeError,
    MFANotSetupError
)

logger = logging.getLogger(__name__)


class UserService:
    """Service for user management and MFA operations."""
    
    def __init__(self, db: Session, config: Optional[AuthConfig] = None):
        """
        Initialize user service.
        
        Args:
            db: Database session
            config: Optional AuthConfig. If None, uses global config.
        """
        self.db = db
        self.config = config or get_config()
        self.mfa_manager = MFAManager(self.config)
    
    def get_user_by_id(self, user_id: int) -> Optional[User]:
        """Get user by ID."""
        return self.db.query(User).filter(User.id == user_id).first()
    
    def get_user_by_username(self, username: str) -> Optional[User]:
        """Get user by username."""
        return self.db.query(User).filter(User.username == username).first()
    
    def get_user_by_email(self, email: str) -> Optional[User]:
        """Get user by email."""
        return self.db.query(User).filter(User.email == email).first()
    
    def update_user(self, user_id: int, updates: Dict[str, Any]) -> Optional[User]:
        """Update user fields."""
        user = self.get_user_by_id(user_id)
        if not user:
            return None
        
        for key, value in updates.items():
            if hasattr(user, key):
                setattr(user, key, value)
        
        self.db.commit()
        self.db.refresh(user)
        return user
    
    # ==================== MFA OPERATIONS ====================
    
    def setup_mfa(self, user_id: int) -> Dict[str, Any]:
        """
        Setup MFA for a user.
        
        Returns:
            Dict with secret, provisioning_uri, and backup_codes
        """
        user = self.get_user_by_id(user_id)
        if not user:
            raise ValueError("User not found")
        
        # Generate MFA credentials using configured issuer name
        secret, uri, backup_codes = self.mfa_manager.setup_mfa(user.email)
        
        # Hash backup codes for storage
        hashed_codes = hash_backup_codes(backup_codes)
        
        # Store in database (not enabled yet)
        self.update_user(user_id, {
            "mfa_secret": secret,
            "backup_codes": hashed_codes
        })
        
        logger.info(f"MFA setup initiated for user {user.username}")
        
        return {
            "secret": secret,
            "provisioning_uri": uri,
            "backup_codes": backup_codes  # Return plain codes once
        }
    
    def verify_and_enable_mfa(self, user_id: int, code: str) -> bool:
        """Verify MFA code and enable MFA for user."""
        user = self.get_user_by_id(user_id)
        if not user or not user.mfa_secret:
            raise MFANotSetupError()
        
        # Verify the code
        if not self.mfa_manager.verify_code(user.mfa_secret, code):
            raise InvalidMFACodeError()
        
        # Enable MFA
        self.update_user(user_id, {
            "mfa_enabled": True,
            "mfa_secret": user.mfa_secret,
            "backup_codes": user.backup_codes
        })
        
        logger.info(f"MFA enabled for user {user.username}")
        return True
    
    def verify_mfa_code(self, user_id: int, code: str) -> bool:
        """
        Verify MFA code during login - accepts TOTP or backup code.
        
        Args:
            user_id: User ID
            code: Either 6-digit TOTP code or 8-character backup code
        
        Returns:
            True if valid, raises exception otherwise
        """
        # Get user
        user = self.get_user_by_id(user_id)
        if not user:
            raise ValueError("User not found")
        
        # Strict MFA enabled check
        if not user.mfa_enabled:
            raise MFANotSetupError()
        
        # Ensure MFA secret exists
        if not user.mfa_secret:
            raise MFANotSetupError()
        
        # Normalize code
        code = code.upper().strip()
        
        # Determine code type and verify
        if len(code) == 6 and code.isdigit():
            # ============ TOTP CODE (6 digits) ============
            if self.mfa_manager.verify_code(user.mfa_secret, code):
                logger.info(f"TOTP code verified for user {user.username}")
                return True
            else:
                raise InvalidMFACodeError()
        
        elif len(code) == 8 and code.isalnum():
            # ============ BACKUP CODE (8 alphanumeric) ============
            
            # Check if backup codes exist
            if not user.backup_codes or len(user.backup_codes) == 0:
                raise InvalidMFACodeError()
            
            # Verify backup code
            if verify_backup_code(code, user.backup_codes):
                # Remove the used backup code
                new_backup_codes = []
                for hashed_code in user.backup_codes:
                    # Keep codes that DON'T match the entered code
                    if not verify_password(code, hashed_code):
                        new_backup_codes.append(hashed_code)
                
                # Update user with remaining codes
                self.update_user(user_id, {"backup_codes": new_backup_codes})
                
                remaining_count = len(new_backup_codes)
                logger.info(f"Backup code used for user {user.username}. Remaining: {remaining_count}")
                
                # Warn if running low on backup codes
                if remaining_count == 0:
                    logger.warning(f"User {user.username} has NO backup codes remaining!")
                elif remaining_count <= 2:
                    logger.warning(f"User {user.username} has only {remaining_count} backup codes remaining")
                
                return True
            else:
                raise InvalidMFACodeError()
        
        else:
            # Invalid format
            raise InvalidMFACodeError()
    
    def disable_mfa(self, user_id: int, password: str) -> bool:
        """Disable MFA for a user and clear ALL MFA data."""
        user = self.get_user_by_id(user_id)
        if not user:
            raise ValueError("User not found")
        
        # Verify password for security
        if not verify_password(password, user.hashed_password):
            raise ValueError("Invalid password")
        
        # Clear BOTH mfa_secret AND backup_codes
        self.update_user(user_id, {
            "mfa_enabled": False,
            "mfa_secret": None,
            "backup_codes": None
        })
        
        logger.info(f"MFA disabled and all secrets cleared for user {user.username}")
        return True
    
    def regenerate_backup_codes(self, user_id: int) -> list[str]:
        """Generate new backup codes."""
        user = self.get_user_by_id(user_id)
        if not user or not user.mfa_enabled:
            raise MFANotSetupError()
        
        # Generate new codes (using configured count)
        new_codes = generate_backup_codes(self.config.mfa_backup_codes_count)
        hashed_codes = hash_backup_codes(new_codes)
        
        # Update database
        self.update_user(user_id, {"backup_codes": hashed_codes})
        
        logger.info(f"Backup codes regenerated for user {user.username}")
        return new_codes
