"""Factory and builder patterns for fair task queue."""

import warnings
from datetime import UTC, datetime, timedelta
from typing import TYPE_CHECKING, Any

# Algorithms are imported in queue.py for default fairness algorithm
# Not needed here since from_config no longer configures algorithms from config
from haolib.background.fair.abstract import (
    AbstractFairQueueStorage,
    AbstractTaskAdapter,
    FairnessAlgorithm,
    GroupKeyExtractor,
    RateLimit,
    RetryStrategy,
    TaskInvocation,
)
from haolib.background.fair.distributed_lock import DistributedLock
from haolib.background.fair.extractors import (
    extract_from_kwargs,
)
from haolib.background.fair.queue import FairTaskQueue
from haolib.background.fair.resilience import ExponentialBackoffRetryStrategy
from haolib.background.fair.results import StorageResultHandler, TaskResultHandler

if TYPE_CHECKING:
    from redis.asyncio import Redis
    from sqlalchemy.ext.asyncio import AsyncSession
    from taskiq import AsyncBroker

try:
    from haolib.configs.fair import FairQueueConfig
except ImportError:
    FairQueueConfig = None  # type: ignore[assignment, misc]


class FairTaskQueueBuilder:
    """Builder for FairTaskQueue with convenient defaults.

    Example:
        ```python
        queue = (
            FairTaskQueueBuilder()
            .with_redis_storage(redis_client)
            .with_taskiq_adapter(broker)
            .with_user_id_extractor()
            .with_default_rate_limit(max_tasks=10, per_time=timedelta(seconds=1))
            .build()
        )
        ```

    """

    def __init__(self) -> None:
        """Initialize builder."""
        self._storage: AbstractFairQueueStorage | None = None
        self._task_adapter: AbstractTaskAdapter | None = None
        self._group_key_extractor: GroupKeyExtractor[TaskInvocation] | None = None
        self._fairness_algorithm: FairnessAlgorithm | None = None
        self._default_rate_limit: RateLimit | None = None
        self._max_queue_size_per_group: int | None = None
        self._max_global_queue_size: int | None = None
        self._default_max_retries: int = 3
        self._scheduler_poll_interval: timedelta = timedelta(seconds=0.1)
        self._scheduler_error_backoff_interval: timedelta = timedelta(seconds=1.0)
        self._scheduler_max_concurrent_tasks: int = 10
        self._retry_strategy: RetryStrategy | None = None
        self._distributed_lock: DistributedLock | None = None
        self._recovery_lock_timeout: timedelta = timedelta(seconds=30)
        self._result_handler: TaskResultHandler | None = None
        self._enable_auto_recovery: bool = False
        self._auto_recovery_interval: timedelta = timedelta(minutes=5)
        self._auto_recovery_max_processing_time: timedelta = timedelta(minutes=5)

    def with_redis_storage(
        self,
        redis: Redis,
        key_prefix: str = "fair_queue",
        task_ttl_seconds: int | None = None,
        operation_timeout: timedelta | None = None,
        enable_distributed_lock: bool = True,
    ) -> FairTaskQueueBuilder:
        """Configure Redis storage.

        Args:
            redis: Redis async client instance
            key_prefix: Prefix for all Redis keys
            task_ttl_seconds: Optional TTL for task keys in seconds
            operation_timeout: Optional timeout for storage operations
            enable_distributed_lock: If True, automatically configure Redis distributed lock

        Returns:
            Self for method chaining

        """
        from haolib.background.fair.storages.redis import RedisFairQueueStorage  # noqa: PLC0415

        self._storage = RedisFairQueueStorage(
            redis=redis,
            key_prefix=key_prefix,
            task_ttl_seconds=task_ttl_seconds,
            operation_timeout=operation_timeout,
        )

        # Automatically configure distributed lock if enabled
        if enable_distributed_lock and self._distributed_lock is None:
            from haolib.background.fair.storages.redis import RedisDistributedLock  # noqa: PLC0415

            self._distributed_lock = RedisDistributedLock(
                redis=redis,
                key_prefix=f"{key_prefix}:lock",
            )

        return self

    def with_database_storage(
        self,
        session: AsyncSession,
        operation_timeout: timedelta | None = None,
    ) -> FairTaskQueueBuilder:
        """Configure database storage using SQLAlchemy.

        Args:
            session: SQLAlchemy async session instance
            operation_timeout: Optional timeout for storage operations (kept for API compatibility)

        Returns:
            Self for method chaining

        Example:
            ```python
            from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
            from sqlalchemy.orm import sessionmaker

            engine = create_async_engine("postgresql+asyncpg://...")
            async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)

            queue = (
                FairTaskQueueBuilder()
                .with_database_storage(async_session())
                .with_taskiq_adapter(broker)
                .with_user_id_extractor()
                .build()
            )
            ```

        Note:
            The session should be configured with appropriate connection pool settings.
            See DatabaseFairQueueStorage documentation for recommended pool configuration.

        """
        from haolib.background.fair.storages.sqlalchemy import DatabaseFairQueueStorage  # noqa: PLC0415

        self._storage = DatabaseFairQueueStorage(
            session=session,
            operation_timeout=operation_timeout,
        )
        return self

    def with_storage(self, storage: AbstractFairQueueStorage) -> FairTaskQueueBuilder:
        """Configure custom storage.

        Args:
            storage: Storage implementation

        Returns:
            Self for method chaining

        """
        self._storage = storage
        return self

    def with_taskiq_adapter(
        self,
        broker: AsyncBroker,
        serializer: Any | None = None,
    ) -> FairTaskQueueBuilder:
        """Configure TaskIQ adapter.

        Args:
            broker: TaskIQ async broker instance
            serializer: Optional task serializer

        Returns:
            Self for method chaining

        """
        from haolib.background.fair.adapters import TaskIQAdapter  # noqa: PLC0415

        self._task_adapter = TaskIQAdapter(broker=broker, serializer=serializer)
        return self

    def with_task_adapter(self, adapter: AbstractTaskAdapter) -> FairTaskQueueBuilder:
        """Configure custom task adapter.

        Args:
            adapter: Task adapter implementation

        Returns:
            Self for method chaining

        """
        self._task_adapter = adapter
        return self

    def with_fairness_algorithm(self, algorithm: FairnessAlgorithm) -> FairTaskQueueBuilder:
        """Configure fairness algorithm.

        Args:
            algorithm: Fairness algorithm implementation

        Returns:
            Self for method chaining

        """
        self._fairness_algorithm = algorithm
        return self

    def with_default_rate_limit(
        self,
        max_tasks: int,
        per_time: timedelta,
    ) -> FairTaskQueueBuilder:
        """Configure default rate limit for groups.

        Args:
            max_tasks: Maximum tasks allowed
            per_time: Time window

        Returns:
            Self for method chaining

        """

        self._default_rate_limit = RateLimit(
            max_tasks=max_tasks,
            per_time=per_time,
            current_count=0,
            window_start=datetime.now(UTC),
        )
        return self

    def with_queue_size_limits(
        self,
        max_per_group: int | None = None,
        max_global: int | None = None,
    ) -> FairTaskQueueBuilder:
        """Configure queue size limits.

        Args:
            max_per_group: Maximum queue size per group (None = unlimited)
            max_global: Maximum global queue size (None = unlimited)

        Returns:
            Self for method chaining

        """
        self._max_queue_size_per_group = max_per_group
        self._max_global_queue_size = max_global
        return self

    def with_retry_config(
        self,
        default_max_retries: int = 3,
        retry_strategy: RetryStrategy | None = None,
    ) -> FairTaskQueueBuilder:
        """Configure retry settings.

        Args:
            default_max_retries: Default maximum retry attempts
            retry_strategy: Retry strategy to use (defaults to ExponentialBackoffRetryStrategy)

        Returns:
            Self for method chaining

        """
        self._default_max_retries = default_max_retries
        self._retry_strategy = retry_strategy
        return self

    def with_scheduler_config(
        self,
        poll_interval: timedelta | None = None,
        error_backoff_interval: timedelta | None = None,
        max_concurrent_tasks: int | None = None,
    ) -> FairTaskQueueBuilder:
        """Configure scheduler settings.

        Args:
            poll_interval: Interval between scheduler iterations
            error_backoff_interval: Backoff interval on errors
            max_concurrent_tasks: Maximum concurrent tasks in scheduler

        Returns:
            Self for method chaining

        """
        if poll_interval is not None:
            self._scheduler_poll_interval = poll_interval
        if error_backoff_interval is not None:
            self._scheduler_error_backoff_interval = error_backoff_interval
        if max_concurrent_tasks is not None:
            self._scheduler_max_concurrent_tasks = max_concurrent_tasks
        return self

    def with_distributed_lock(
        self,
        lock: DistributedLock,
        recovery_lock_timeout: timedelta = timedelta(seconds=30),
    ) -> FairTaskQueueBuilder:
        """Configure distributed lock for multi-instance coordination.

        Args:
            lock: Distributed lock implementation
            recovery_lock_timeout: Timeout for acquiring recovery lock

        Returns:
            Self for method chaining

        Example:
            ```python
            from haolib.background.fair.storages.redis import RedisDistributedLock

            lock = RedisDistributedLock(redis_client)
            builder.with_distributed_lock(lock)
            ```

        """
        self._distributed_lock = lock
        self._recovery_lock_timeout = recovery_lock_timeout
        return self

    def with_result_handler(
        self,
        result_handler: TaskResultHandler,
    ) -> FairTaskQueueBuilder:
        """Configure result handler for storing task results.

        Args:
            result_handler: Result handler implementation

        Returns:
            Self for method chaining

        Example:
            ```python
            from haolib.background.fair.result_handler import StorageResultHandler

            result_handler = StorageResultHandler(storage, default_ttl_seconds=86400)
            builder.with_result_handler(result_handler)
            ```

        """
        self._result_handler = result_handler
        return self

    def with_auto_result_storage(
        self,
        result_ttl_seconds: int | None = None,
    ) -> FairTaskQueueBuilder:
        """Enable automatic result storage using storage backend.

        This is a convenience method that creates a StorageResultHandler
        using the configured storage backend.

        Args:
            result_ttl_seconds: Optional TTL for stored results (None = use storage default)

        Returns:
            Self for method chaining

        Example:
            ```python
            # Automatically store results with 24 hour TTL
            builder.with_auto_result_storage(result_ttl_seconds=86400)
            ```

        """
        if self._storage is None:
            raise ValueError("Storage must be configured before enabling auto result storage")
        self._result_handler = StorageResultHandler(
            storage=self._storage,
            default_ttl_seconds=result_ttl_seconds,
        )
        return self

    def with_auto_recovery(
        self,
        recovery_interval: timedelta = timedelta(minutes=5),
        max_processing_time: timedelta = timedelta(minutes=5),
    ) -> FairTaskQueueBuilder:
        """Enable automatic periodic recovery of stuck tasks.

        Args:
            recovery_interval: Interval between recovery runs
            max_processing_time: Maximum processing time before task is considered stuck

        Returns:
            Self for method chaining

        Example:
            ```python
            # Recover stuck tasks every 5 minutes
            builder.with_auto_recovery(
                recovery_interval=timedelta(minutes=5),
                max_processing_time=timedelta(minutes=5),
            )
            ```

        """
        self._enable_auto_recovery = True
        self._auto_recovery_interval = recovery_interval
        self._auto_recovery_max_processing_time = max_processing_time
        return self

    def with_user_id_extractor(
        self,
        user_id_key: str = "user_id",
        fallback_keys: list[str] | None = None,
    ) -> FairTaskQueueBuilder:
        """Configure user ID extractor from TaskInvocation kwargs.

        Args:
            user_id_key: Key to extract user ID from TaskInvocation kwargs
            fallback_keys: Optional fallback keys to try if primary key not found

        Returns:
            Self for method chaining

        Example:
            ```python
            builder.with_user_id_extractor()  # Extracts from TaskInvocation.kwargs["user_id"]
            # Extracts from TaskInvocation.kwargs["tenant_id"]
            builder.with_user_id_extractor("tenant_id", fallback_keys=["user_id"])
            ```

        """
        # Use functional extractor (more efficient)
        self._group_key_extractor = extract_from_kwargs(
            user_id_key,
            fallback_keys=fallback_keys or [],
        )
        return self

    def with_group_key_extractor(
        self,
        extractor: GroupKeyExtractor[TaskInvocation],
    ) -> FairTaskQueueBuilder:
        """Configure custom group key extractor.

        Args:
            extractor: Group key extractor implementation

        Returns:
            Self for method chaining

        Example:
            ```python
            # Functional approach (recommended)
            from haolib.background.fair.extractors import extract_from_kwargs, compose_extractors

            extractor = extract_from_kwargs("user_id", fallback_keys=["tenant_id"])
            builder.with_group_key_extractor(extractor)

            # Or compose multiple extractors
            extractor = compose_extractors(
                extract_from_kwargs("user_id", fallback_keys=["tenant_id"]),
                extract_from_args(0),
            )
            builder.with_group_key_extractor(extractor)

            # Class-based approach (backward compatibility)
            from haolib.background.fair.extractors import TaskInvocationGroupKeyExtractor

            extractor = TaskInvocationGroupKeyExtractor(
                group_id_key="user_id",
                fallback_keys=["tenant_id"],
            )
            builder.with_group_key_extractor(extractor)
            ```

        """
        self._group_key_extractor = extractor
        return self

    @classmethod
    def from_config(
        cls,
        config: Any,  # FairQueueConfig (using Any to avoid import issues when config is optional)
        redis: Any | None = None,  # Redis | None
        broker: Any | None = None,  # AsyncBroker | None
        group_key_extractor: GroupKeyExtractor[TaskInvocation] | None = None,
    ) -> FairTaskQueueBuilder:
        """Create builder from FairQueueConfig.

        Args:
            config: FairQueueConfig instance
            redis: Optional Redis client (required if storage_type is redis)
            broker: Optional TaskIQ broker (required if using TaskIQ)
            group_key_extractor: Optional group key extractor

        Returns:
            Configured builder instance

        Raises:
            ValueError: If required dependencies are missing

        Example:
            ```python
            config = FairQueueConfig()
            builder = FairTaskQueueBuilder.from_config(config, redis=redis, broker=broker)
            queue = builder.build()
            ```

        """
        if FairQueueConfig is None:
            raise ValueError("FairQueueConfig is not available. Install haolib[config]")

        builder = cls()

        # Configure storage - must be provided separately as runtime objects
        # Config only contains settings, not runtime dependencies
        if redis is not None:
            builder.with_redis_storage(redis=redis, key_prefix="fair_queue")
        # Note: Database storage requires session which must be provided via with_database_storage()

        # Configure task adapter - must be provided separately
        if broker is not None:
            builder.with_taskiq_adapter(broker=broker)

        # Configure group key extractor
        if group_key_extractor is not None:
            builder.with_group_key_extractor(group_key_extractor)
        else:
            # Default to user_id extractor
            builder.with_user_id_extractor()

        # Configure queue size limits (from config)
        builder.with_queue_size_limits(
            max_per_group=config.max_queue_size_per_group,
            max_global=config.max_global_queue_size,
        )

        # Configure retry strategy
        retry_strategy = ExponentialBackoffRetryStrategy()
        builder.with_retry_config(
            default_max_retries=config.default_max_retries,
            retry_strategy=retry_strategy,
        )

        # Configure scheduler
        builder.with_scheduler_config(
            poll_interval=config.scheduler_poll_interval,
            error_backoff_interval=config.scheduler_error_backoff_interval,
            max_concurrent_tasks=config.scheduler_max_concurrent_tasks,
        )

        # Configure auto-recovery if enabled
        if config.enable_auto_recovery:
            builder.with_auto_recovery(
                recovery_interval=config.auto_recovery_interval,
                max_processing_time=config.auto_recovery_max_processing_time,
            )

        # Configure auto result storage if enabled
        if config.enable_auto_result_storage:
            builder.with_auto_result_storage(result_ttl_seconds=config.result_ttl_seconds)

        return builder

    @classmethod
    def from_redis_and_taskiq(
        cls,
        redis: Redis,
        broker: AsyncBroker,
        group_key_extractor: GroupKeyExtractor[TaskInvocation] | None = None,
        **kwargs: Any,
    ) -> FairTaskQueueBuilder:
        """Create builder with Redis storage and TaskIQ adapter.

        Args:
            redis: Redis async client instance
            broker: TaskIQ async broker instance
            group_key_extractor: Optional group key extractor (defaults to user_id extractor)
            **kwargs: Additional builder configuration options

        Returns:
            Configured builder instance

        Example:
            ```python
            builder = FairTaskQueueBuilder.from_redis_and_taskiq(redis, broker)
            queue = builder.build()
            ```

        """
        builder = cls()
        builder.with_redis_storage(redis)
        builder.with_taskiq_adapter(broker)

        if group_key_extractor is not None:
            builder.with_group_key_extractor(group_key_extractor)
        else:
            builder.with_user_id_extractor()

        # Apply additional kwargs
        if "default_rate_limit" in kwargs:
            max_tasks = kwargs.pop("default_rate_limit_max_tasks", 10)
            per_time = kwargs.pop("default_rate_limit_per_time", timedelta(seconds=1))
            builder.with_default_rate_limit(max_tasks=max_tasks, per_time=per_time)

        if "max_queue_size_per_group" in kwargs or "max_global_queue_size" in kwargs:
            builder.with_queue_size_limits(
                max_per_group=kwargs.pop("max_queue_size_per_group", None),
                max_global=kwargs.pop("max_global_queue_size", None),
            )

        if "default_max_retries" in kwargs or "retry_strategy" in kwargs:
            builder.with_retry_config(
                default_max_retries=kwargs.pop("default_max_retries", 3),
                retry_strategy=kwargs.pop("retry_strategy", None),
            )

        scheduler_keys = [
            "scheduler_poll_interval",
            "scheduler_error_backoff_interval",
            "scheduler_max_concurrent_tasks",
        ]
        if any(k in kwargs for k in scheduler_keys):
            builder.with_scheduler_config(
                poll_interval=kwargs.pop("scheduler_poll_interval", None),
                error_backoff_interval=kwargs.pop("scheduler_error_backoff_interval", None),
                max_concurrent_tasks=kwargs.pop("scheduler_max_concurrent_tasks", None),
            )

        if "fairness_algorithm" in kwargs:
            builder.with_fairness_algorithm(kwargs.pop("fairness_algorithm"))

        if kwargs:
            # Warn about unused kwargs
            unused_keys = list(kwargs.keys())
            warnings.warn(f"Unused kwargs: {unused_keys}", UserWarning, stacklevel=2)

        return builder

    def build(self) -> FairTaskQueue:
        """Build FairTaskQueue instance.

        Returns:
            Configured FairTaskQueue instance

        Raises:
            ValueError: If required components are not configured, with helpful error messages

        Example:
            ```python
            queue = (
                FairTaskQueueBuilder()
                .with_redis_storage(redis)
                .with_taskiq_adapter(broker)
                .with_user_id_extractor()
                .build()
            )
            ```

        """
        # Validate required components with helpful error messages
        if self._storage is None:
            raise ValueError(
                "Storage must be configured. "
                "Use .with_redis_storage(redis), .with_database_storage(session), "
                "or .with_storage(custom_storage) to configure storage."
            )
        if self._task_adapter is None:
            raise ValueError(
                "Task adapter must be configured. "
                "Use .with_taskiq_adapter(broker) or .with_task_adapter(custom_adapter) "
                "to configure task adapter."
            )
        if self._group_key_extractor is None:
            raise ValueError(
                "Group key extractor must be configured. "
                "Use .with_user_id_extractor() or .with_group_key_extractor(extractor)."
            )

        return FairTaskQueue(
            storage=self._storage,
            task_adapter=self._task_adapter,
            group_key_extractor=self._group_key_extractor,
            fairness_algorithm=self._fairness_algorithm,
            default_rate_limit=self._default_rate_limit,
            max_queue_size_per_group=self._max_queue_size_per_group,
            max_global_queue_size=self._max_global_queue_size,
            default_max_retries=self._default_max_retries,
            scheduler_poll_interval=self._scheduler_poll_interval,
            scheduler_error_backoff_interval=self._scheduler_error_backoff_interval,
            scheduler_max_concurrent_tasks=self._scheduler_max_concurrent_tasks,
            retry_strategy=self._retry_strategy,
            distributed_lock=self._distributed_lock,
            recovery_lock_timeout=self._recovery_lock_timeout,
            result_handler=self._result_handler,
            enable_auto_recovery=self._enable_auto_recovery,
            auto_recovery_interval=self._auto_recovery_interval,
            auto_recovery_max_processing_time=self._auto_recovery_max_processing_time,
        )


def create_fair_task_queue(
    storage: AbstractFairQueueStorage,
    task_adapter: AbstractTaskAdapter,
    group_key_extractor: GroupKeyExtractor[TaskInvocation],
    **kwargs: Any,
) -> FairTaskQueue:
    """Create a FairTaskQueue with given configuration.

    This is a convenience function for creating a queue without using the builder.

    Args:
        storage: Storage backend for tasks
        task_adapter: Task adapter for task execution
        group_key_extractor: Function/class to extract group ID from task
        **kwargs: Additional arguments passed to FairTaskQueue constructor

    Returns:
        Configured FairTaskQueue instance

    """
    return FairTaskQueue(
        storage=storage,
        task_adapter=task_adapter,
        group_key_extractor=group_key_extractor,
        **kwargs,
    )


def create_fair_task_queue_quick(
    redis: Redis,
    broker: AsyncBroker,
    group_key_extractor: GroupKeyExtractor[TaskInvocation] | None = None,
    **kwargs: Any,
) -> FairTaskQueue:
    """Quick start factory function for creating a FairTaskQueue.

    This is the simplest way to create a queue with sensible defaults.
    Automatically configures Redis storage and TaskIQ adapter.

    Args:
        redis: Redis async client instance
        broker: TaskIQ async broker instance
        group_key_extractor: Optional group key extractor (defaults to user_id extractor)
        **kwargs: Additional configuration options (rate_limit, max_retries, etc.)

    Returns:
        Configured FairTaskQueue instance ready to use

    Example:
        ```python
        from redis.asyncio import Redis
        from taskiq import AsyncBroker
        from haolib.background.fair import create_fair_task_queue_quick

        redis = Redis()
        broker = AsyncBroker()

        # Simplest usage - uses defaults
        queue = create_fair_task_queue_quick(redis, broker)

        # With custom rate limit
        from datetime import timedelta
        queue = create_fair_task_queue_quick(
            redis,
            broker,
            default_rate_limit=RateLimit(max_tasks=10, per_time=timedelta(seconds=1)),
        )

        async with queue:
            task_id = await queue.enqueue(task, group_id="user_123")
        ```

    """
    builder = FairTaskQueueBuilder.from_redis_and_taskiq(
        redis=redis,
        broker=broker,
        group_key_extractor=group_key_extractor,
        **kwargs,
    )
    return builder.build()
