"""
OMNIWEB Auth - Zero-configuration authentication system
Author: Juste Elysée MALANDILA

Features:
- JWT & Session auth
- OAuth2 (Google, GitHub, etc.)
- Magic links
- 2FA support
- Password reset
- Email verification
"""

from typing import Optional, List, Dict, Any, Callable
import secrets
import hashlib
import time
from datetime import datetime, timedelta
import jwt
from pydantic import BaseModel, Field


class User(BaseModel):
    """User model for authentication."""
    id: int
    email: str
    username: Optional[str] = None
    password_hash: Optional[str] = None
    is_active: bool = True
    is_verified: bool = False
    created_at: datetime = Field(default_factory=datetime.now)
    last_login: Optional[datetime] = None


class Token(BaseModel):
    """JWT Token model."""
    access_token: str
    token_type: str = "bearer"
    expires_in: int = 3600


class AuthConfig:
    """Authentication configuration."""
    
    def __init__(
        self,
        secret_key: str = None,
        algorithm: str = "HS256",
        access_token_expire: int = 3600,
        refresh_token_expire: int = 2592000,  # 30 days
        enable_oauth: bool = False,
        oauth_providers: List[str] = None,
        enable_magic_links: bool = False,
        enable_2fa: bool = False,
        password_min_length: int = 8,
    ):
        self.secret_key = secret_key or secrets.token_urlsafe(32)
        self.algorithm = algorithm
        self.access_token_expire = access_token_expire
        self.refresh_token_expire = refresh_token_expire
        self.enable_oauth = enable_oauth
        self.oauth_providers = oauth_providers or []
        self.enable_magic_links = enable_magic_links
        self.enable_2fa = enable_2fa
        self.password_min_length = password_min_length


class AuthSystem:
    """Zero-config authentication system."""
    
    def __init__(self, config: AuthConfig = None):
        self.config = config or AuthConfig()
        self._users: Dict[str, User] = {}  # In-memory (use DB in production)
        self._sessions: Dict[str, Dict] = {}
        self._magic_links: Dict[str, str] = {}
        self._2fa_codes: Dict[str, str] = {}
    
    def hash_password(self, password: str) -> str:
        """Hash password with salt."""
        salt = secrets.token_hex(16)
        pwd_hash = hashlib.pbkdf2_hmac(
            'sha256',
            password.encode('utf-8'),
            salt.encode('utf-8'),
            100000
        )
        return f"{salt}${pwd_hash.hex()}"
    
    def verify_password(self, password: str, password_hash: str) -> bool:
        """Verify password against hash."""
        try:
            salt, pwd_hash = password_hash.split('$')
            new_hash = hashlib.pbkdf2_hmac(
                'sha256',
                password.encode('utf-8'),
                salt.encode('utf-8'),
                100000
            )
            return new_hash.hex() == pwd_hash
        except Exception:
            return False
    
    def create_token(self, user_id: int, expires_delta: int = None) -> Token:
        """Create JWT token."""
        if expires_delta is None:
            expires_delta = self.config.access_token_expire
        
        expire = datetime.utcnow() + timedelta(seconds=expires_delta)
        payload = {
            "sub": str(user_id),
            "exp": expire,
            "iat": datetime.utcnow(),
        }
        
        token = jwt.encode(
            payload,
            self.config.secret_key,
            algorithm=self.config.algorithm
        )
        
        return Token(
            access_token=token,
            expires_in=expires_delta
        )
    
    def verify_token(self, token: str) -> Optional[int]:
        """Verify JWT token and return user_id."""
        try:
            payload = jwt.decode(
                token,
                self.config.secret_key,
                algorithms=[self.config.algorithm]
            )
            user_id = int(payload.get("sub"))
            return user_id
        except jwt.ExpiredSignatureError:
            return None
        except jwt.JWTError:
            return None
    
    async def register(
        self,
        email: str,
        password: str,
        username: Optional[str] = None
    ) -> User:
        """Register new user."""
        # Check if user exists
        if email in self._users:
            raise ValueError("User already exists")
        
        # Validate password
        if len(password) < self.config.password_min_length:
            raise ValueError(
                f"Password must be at least {self.config.password_min_length} characters"
            )
        
        # Create user
        user = User(
            id=len(self._users) + 1,
            email=email,
            username=username or email.split('@')[0],
            password_hash=self.hash_password(password),
        )
        
        self._users[email] = user
        return user
    
    async def login(self, email: str, password: str) -> Token:
        """Login user and return token."""
        user = self._users.get(email)
        
        if not user:
            raise ValueError("Invalid credentials")
        
        if not self.verify_password(password, user.password_hash):
            raise ValueError("Invalid credentials")
        
        if not user.is_active:
            raise ValueError("User is inactive")
        
        # Update last login
        user.last_login = datetime.now()
        
        return self.create_token(user.id)
    
    async def get_current_user(self, token: str) -> Optional[User]:
        """Get current user from token."""
        user_id = self.verify_token(token)
        if not user_id:
            return None
        
        # Find user by ID
        for user in self._users.values():
            if user.id == user_id:
                return user
        
        return None
    
    def create_magic_link(self, email: str) -> str:
        """Create magic link for passwordless login."""
        token = secrets.token_urlsafe(32)
        self._magic_links[token] = email
        return token
    
    async def verify_magic_link(self, token: str) -> Optional[Token]:
        """Verify magic link and return auth token."""
        email = self._magic_links.get(token)
        if not email:
            return None
        
        user = self._users.get(email)
        if not user:
            return None
        
        # Remove magic link after use
        del self._magic_links[token]
        
        return self.create_token(user.id)
    
    def generate_2fa_code(self, user_id: int) -> str:
        """Generate 2FA code."""
        code = str(secrets.randbelow(1000000)).zfill(6)
        self._2fa_codes[str(user_id)] = code
        return code
    
    def verify_2fa_code(self, user_id: int, code: str) -> bool:
        """Verify 2FA code."""
        stored_code = self._2fa_codes.get(str(user_id))
        if stored_code == code:
            del self._2fa_codes[str(user_id)]
            return True
        return False


def enable_auth(
    app,
    providers: List[str] = None,
    secret_key: str = None,
    enable_magic_links: bool = True,
    enable_2fa: bool = False,
    **kwargs
) -> AuthSystem:
    """
    Enable authentication on OMNIWEB app.
    
    Example:
        >>> from omniweb import OmniWeb
        >>> from omniweb.auth import enable_auth
        >>> 
        >>> app = OmniWeb()
        >>> auth = enable_auth(app, providers=["google", "github"])
        >>> 
        >>> # Auto-creates routes:
        >>> # POST /auth/register
        >>> # POST /auth/login
        >>> # POST /auth/logout
        >>> # GET /auth/me
        >>> # POST /auth/magic-link
        >>> # GET /auth/google
        >>> # GET /auth/github
    
    Args:
        app: OMNIWEB application
        providers: OAuth providers (e.g., ["google", "github"])
        secret_key: JWT secret key
        enable_magic_links: Enable passwordless login
        enable_2fa: Enable two-factor authentication
    
    Returns:
        AuthSystem instance
    """
    config = AuthConfig(
        secret_key=secret_key,
        oauth_providers=providers or [],
        enable_magic_links=enable_magic_links,
        enable_2fa=enable_2fa,
        **kwargs
    )
    
    auth = AuthSystem(config)
    
    # Register auth routes
    @app.post("/auth/register")
    async def register(email: str, password: str, username: str = None):
        """Register new user."""
        try:
            user = await auth.register(email, password, username)
            return {
                "message": "User registered successfully",
                "user": {
                    "id": user.id,
                    "email": user.email,
                    "username": user.username
                }
            }
        except ValueError as e:
            return {"error": str(e)}, 400
    
    @app.post("/auth/login")
    async def login(email: str, password: str):
        """Login user."""
        try:
            token = await auth.login(email, password)
            return token.dict()
        except ValueError as e:
            return {"error": str(e)}, 401
    
    @app.get("/auth/me")
    async def get_me(request):
        """Get current user."""
        # Extract token from Authorization header
        auth_header = request.headers.get("Authorization", "")
        if not auth_header.startswith("Bearer "):
            return {"error": "Missing authorization"}, 401
        
        token = auth_header.replace("Bearer ", "")
        user = await auth.get_current_user(token)
        
        if not user:
            return {"error": "Invalid token"}, 401
        
        return {
            "id": user.id,
            "email": user.email,
            "username": user.username,
            "is_verified": user.is_verified
        }
    
    @app.post("/auth/logout")
    async def logout():
        """Logout user."""
        return {"message": "Logged out successfully"}
    
    if config.enable_magic_links:
        @app.post("/auth/magic-link")
        async def request_magic_link(email: str):
            """Request magic link for passwordless login."""
            token = auth.create_magic_link(email)
            # In production, send email with magic link
            magic_link = f"/auth/magic-link/verify?token={token}"
            return {
                "message": "Magic link sent",
                "link": magic_link  # Only for dev
            }
        
        @app.get("/auth/magic-link/verify")
        async def verify_magic_link(token: str):
            """Verify magic link."""
            auth_token = await auth.verify_magic_link(token)
            if not auth_token:
                return {"error": "Invalid magic link"}, 401
            return auth_token.dict()
    
    if config.enable_2fa:
        @app.post("/auth/2fa/generate")
        async def generate_2fa(request):
            """Generate 2FA code."""
            # Get current user
            auth_header = request.headers.get("Authorization", "")
            token = auth_header.replace("Bearer ", "")
            user = await auth.get_current_user(token)
            
            if not user:
                return {"error": "Unauthorized"}, 401
            
            code = auth.generate_2fa_code(user.id)
            # In production, send code via SMS/email
            return {
                "message": "2FA code sent",
                "code": code  # Only for dev
            }
        
        @app.post("/auth/2fa/verify")
        async def verify_2fa(request, code: str):
            """Verify 2FA code."""
            auth_header = request.headers.get("Authorization", "")
            token = auth_header.replace("Bearer ", "")
            user = await auth.get_current_user(token)
            
            if not user:
                return {"error": "Unauthorized"}, 401
            
            if auth.verify_2fa_code(user.id, code):
                return {"message": "2FA verified"}
            return {"error": "Invalid code"}, 401
    
    # Store auth in app
    app.auth = auth
    
    return auth


# Dependency for protecting routes
def requires_auth(func: Callable) -> Callable:
    """
    Decorator to protect routes with authentication.
    
    Example:
        >>> from omniweb.auth import requires_auth
        >>> 
        >>> @app.get("/protected")
        >>> @requires_auth
        >>> async def protected_route(request):
        >>>     return {"user": request.user}
    """
    async def wrapper(request, *args, **kwargs):
        auth_header = request.headers.get("Authorization", "")
        if not auth_header.startswith("Bearer "):
            return {"error": "Missing authorization"}, 401
        
        token = auth_header.replace("Bearer ", "")
        
        # Get auth system from app
        if not hasattr(request.app, 'auth'):
            return {"error": "Auth not configured"}, 500
        
        user = await request.app.auth.get_current_user(token)
        if not user:
            return {"error": "Invalid token"}, 401
        
        # Attach user to request
        request.user = user
        
        return await func(request, *args, **kwargs)
    
    return wrapper
