"""
Authentication service for user login, logout, and token management
"""
from typing import Optional, Dict, Any
from datetime import timedelta
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from passlib.hash import bcrypt
import structlog

from models.user import User
from models.audit_log import AuditLog
from utils.jwt_utils import create_access_token, create_refresh_token, verify_token, decode_token
from clients.redis_client import redis_client
from config import settings

logger = structlog.get_logger()


class AuthService:
    """Service for authentication operations"""

    def __init__(self, db_session: AsyncSession):
        self.db = db_session

    async def login(
        self,
        username: str,
        password: str,
        ip_address: Optional[str] = None,
        user_agent: Optional[str] = None
    ) -> Optional[Dict[str, Any]]:
        """
        Authenticate user and generate tokens

        Args:
            username: Username to authenticate
            password: Plain text password
            ip_address: Client IP address for audit logging
            user_agent: Client user agent for audit logging

        Returns:
            Dictionary with tokens and user info, or None if authentication failed
        """
        logger.info("login_attempt", username=username, ip_address=ip_address)

        # Find user by username
        result = await self.db.execute(
            select(User).where(User.username == username)
        )
        user = result.scalar_one_or_none()

        if not user:
            logger.warning("login_failed_user_not_found", username=username)
            # Create audit log for failed login
            await self._create_audit_log(
                action="auth.login",
                target=username,
                outcome="failure",
                details={"reason": "user_not_found"},
                ip_address=ip_address,
                user_agent=user_agent
            )
            return None

        # Verify password
        if not await self.verify_password(password, user.password_hash):
            logger.warning("login_failed_invalid_password", username=username, user_id=str(user.id))
            # Create audit log for failed login
            await self._create_audit_log(
                action="auth.login",
                target=username,
                outcome="failure",
                details={"reason": "invalid_password"},
                ip_address=ip_address,
                user_agent=user_agent,
                user_id=user.id
            )
            return None

        # Generate tokens
        token_data = {
            "sub": str(user.id),
            "username": user.username,
            "role": user.role.value
        }

        access_token = create_access_token(token_data)
        refresh_token = create_refresh_token(token_data)

        logger.info("login_success", username=username, user_id=str(user.id), role=user.role.value)

        # Create audit log for successful login
        await self._create_audit_log(
            action="auth.login",
            target=username,
            outcome="success",
            details={"role": user.role.value},
            ip_address=ip_address,
            user_agent=user_agent,
            user_id=user.id
        )

        # Return token response
        return {
            "access_token": access_token,
            "refresh_token": refresh_token,
            "token_type": "bearer",
            "expires_in": settings.JWT_ACCESS_TOKEN_EXPIRE_MINUTES * 60,  # Convert to seconds
            "user": {
                "id": str(user.id),
                "username": user.username,
                "role": user.role.value,
                "created_at": user.created_at,
                "updated_at": user.updated_at
            }
        }

    async def logout(
        self,
        token: str,
        ip_address: Optional[str] = None,
        user_agent: Optional[str] = None
    ) -> bool:
        """
        Logout user by blacklisting their token

        Args:
            token: JWT access token to blacklist
            ip_address: Client IP address for audit logging
            user_agent: Client user agent for audit logging

        Returns:
            True if logout successful, False otherwise
        """
        # Decode and verify token
        payload = decode_token(token)
        if not payload:
            logger.warning("logout_failed_invalid_token")
            return False

        user_id = payload.get("sub")
        username = payload.get("username")

        # Calculate remaining TTL for token
        exp = payload.get("exp")
        if not exp:
            logger.warning("logout_failed_no_expiration", user_id=user_id)
            return False

        # Blacklist token in Redis with TTL matching token expiration
        from datetime import datetime
        remaining_seconds = int(exp - datetime.utcnow().timestamp())

        if remaining_seconds > 0:
            blacklist_key = f"blacklist:{token}"
            await redis_client.set(blacklist_key, "1", expire=remaining_seconds)
            logger.info("token_blacklisted", user_id=user_id, username=username, ttl=remaining_seconds)

        # Create audit log for logout
        await self._create_audit_log(
            action="auth.logout",
            target=username,
            outcome="success",
            ip_address=ip_address,
            user_agent=user_agent,
            user_id=user_id
        )

        logger.info("logout_success", user_id=user_id, username=username)
        return True

    async def validate_token(self, token: str) -> Optional[User]:
        """
        Validate JWT token and return user if valid

        Args:
            token: JWT access token

        Returns:
            User object if token is valid, None otherwise
        """
        # Verify token signature and expiration
        payload = verify_token(token, token_type="access")
        if not payload:
            return None

        # Check if token is blacklisted
        blacklist_key = f"blacklist:{token}"
        is_blacklisted = await redis_client.get(blacklist_key)
        if is_blacklisted:
            logger.warning("token_blacklisted_validation_failed", user_id=payload.get("sub"))
            return None

        # Get user from database
        user_id = payload.get("sub")
        if not user_id:
            return None

        result = await self.db.execute(
            select(User).where(User.id == user_id)
        )
        user = result.scalar_one_or_none()

        return user

    async def refresh_access_token(
        self,
        refresh_token: str,
        ip_address: Optional[str] = None
    ) -> Optional[Dict[str, Any]]:
        """
        Generate new access token from refresh token

        Args:
            refresh_token: JWT refresh token
            ip_address: Client IP address for audit logging

        Returns:
            Dictionary with new access token, or None if refresh failed
        """
        # Verify refresh token
        payload = verify_token(refresh_token, token_type="refresh")
        if not payload:
            logger.warning("refresh_failed_invalid_token")
            return None

        # Check if refresh token is blacklisted
        blacklist_key = f"blacklist:{refresh_token}"
        is_blacklisted = await redis_client.get(blacklist_key)
        if is_blacklisted:
            logger.warning("refresh_failed_token_blacklisted", user_id=payload.get("sub"))
            return None

        # Generate new access token
        token_data = {
            "sub": payload.get("sub"),
            "username": payload.get("username"),
            "role": payload.get("role")
        }

        access_token = create_access_token(token_data)

        logger.info("token_refreshed", user_id=payload.get("sub"), username=payload.get("username"))

        return {
            "access_token": access_token,
            "token_type": "bearer",
            "expires_in": settings.JWT_ACCESS_TOKEN_EXPIRE_MINUTES * 60
        }

    async def hash_password(self, password: str) -> str:
        """
        Hash password using bcrypt

        Args:
            password: Plain text password

        Returns:
            Bcrypt hashed password
        """
        return bcrypt.hash(password)

    async def verify_password(self, plain_password: str, hashed_password: str) -> bool:
        """
        Verify password against hash

        Args:
            plain_password: Plain text password
            hashed_password: Bcrypt hashed password

        Returns:
            True if password matches, False otherwise
        """
        try:
            return bcrypt.verify(plain_password, hashed_password)
        except Exception as e:
            logger.error("password_verification_error", error=str(e))
            return False

    async def _create_audit_log(
        self,
        action: str,
        target: str,
        outcome: str,
        details: Optional[Dict[str, Any]] = None,
        ip_address: Optional[str] = None,
        user_agent: Optional[str] = None,
        user_id: Optional[str] = None
    ) -> None:
        """
        Create audit log entry

        Args:
            action: Action name (e.g., "auth.login")
            target: Target of action (e.g., username)
            outcome: Outcome ("success", "failure", "error")
            details: Additional details as dictionary
            ip_address: Client IP address
            user_agent: Client user agent
            user_id: User UUID (if available)
        """
        try:
            audit_log = AuditLog(
                user_id=user_id,
                action=action,
                target=target,
                outcome=outcome,
                details=details,
                ip_address=ip_address,
                user_agent=user_agent
            )
            self.db.add(audit_log)
            await self.db.commit()
        except Exception as e:
            logger.error("audit_log_creation_failed", action=action, error=str(e))
            # Don't let audit log failure break the operation
            await self.db.rollback()
