"""Customization points for fair task queue.

This module provides protocols and base classes for customizing
dependency resolution, task prioritization, and other behaviors.
"""

from typing import Any, NoReturn, Protocol

from haolib.background.fair.abstract import FairTask, GroupQueueState
from haolib.background.fair.exceptions import FairQueueInvalidTaskError


class DependencyResolver(Protocol):
    """Protocol for custom dependency resolution strategies.

    Allows customization of how task dependencies are validated and resolved.
    Default implementation performs DAG validation, but custom implementations
    can add additional logic (e.g., dependency versioning, conditional dependencies).

    Example:
        ```python
        class VersionedDependencyResolver:
            async def validate_dependencies(
                self,
                task_id: str,
                depends_on: list[str],
                group_id: str | None,
                storage: Any,
            ) -> None:
                # Custom validation logic
                for dep_id in depends_on:
                    # Check if dependency version is compatible
                    dep_task = await storage.get_task(dep_id, "")
                    if not is_compatible(dep_task.version, task.version):
                        raise FairQueueInvalidTaskError(...)
        ```

    """

    async def validate_dependencies(
        self,
        task_id: str,
        depends_on: list[str],
        group_id: str | None,
        storage: Any,  # AbstractFairQueueStorage
    ) -> None:
        """Validate task dependencies.

        Args:
            task_id: Task ID being validated
            depends_on: List of task IDs this task depends on
            group_id: Group ID of the task
            storage: Storage backend for checking dependencies

        Raises:
            FairQueueInvalidTaskError: If dependencies are invalid

        """
        ...


class TaskPrioritizer(Protocol):
    """Protocol for custom task prioritization strategies.

    Allows customization of how tasks are prioritized beyond the simple
    priority field. Can implement complex prioritization logic based on
    task metadata, group state, or external factors.

    Example:
        ```python
        class DynamicPrioritizer:
            def calculate_priority(
                self,
                task: FairTask,
                group_state: GroupQueueState | None,
            ) -> int:
                # Base priority
                base_priority = task.priority

                # Boost priority for tasks in groups with long queues
                if group_state and group_state.queue_length > 100:
                    base_priority += 10

                # Boost priority for retries
                if task.retry_count > 0:
                    base_priority += 5

                return base_priority
        ```

    """

    def calculate_priority(
        self,
        task: FairTask,
        group_state: GroupQueueState | None,
    ) -> int:
        """Calculate effective priority for a task.

        Args:
            task: Task to prioritize
            group_state: Current state of the task's group queue

        Returns:
            Effective priority value (higher = more priority)

        Note:
            This method is called during task enqueue and dequeue to determine
            the effective priority. The returned value should be used for
            sorting tasks within a group.

        """
        ...


class GroupWeightCalculator(Protocol):
    """Protocol for custom group weight calculation.

    Allows customization of how group weights are calculated for fairness
    algorithms. Default implementation uses a fixed weight, but custom
    implementations can make weights dynamic based on group characteristics.

    Example:
        ```python
        class DynamicWeightCalculator:
            def calculate_weight(
                self,
                group_id: str,
                group_state: GroupQueueState,
            ) -> float:
                # Higher weight for premium groups
                if group_id.startswith("premium_"):
                    return 2.0

                # Lower weight for groups with many pending tasks
                if group_state.queue_length > 1000:
                    return 0.5

                return 1.0
        ```

    """

    def calculate_weight(
        self,
        group_id: str,
        group_state: GroupQueueState,
    ) -> float:
        """Calculate weight for a group.

        Args:
            group_id: Group ID
            group_state: Current state of the group's queue

        Returns:
            Weight value (higher = more fair share)

        Note:
            This method is called by fairness algorithms to determine
            how much processing time a group should receive relative to others.

        """
        ...


class DefaultDependencyResolver:
    """Default dependency resolver with DAG validation."""

    def __init__(
        self,
        max_depth: int = 100,
        max_task_id_length: int = 255,
    ) -> None:
        """Initialize default dependency resolver.

        Args:
            max_depth: Maximum dependency chain depth
            max_task_id_length: Maximum length for dependency task IDs

        """
        self._max_depth = max_depth
        self._max_task_id_length = max_task_id_length

    def _raise_circular_dependency_error(
        self, current_task_id: str, dep_task_id: str, group_id: str | None
    ) -> NoReturn:
        """Raise error for circular dependency.

        Args:
            current_task_id: Current task ID
            dep_task_id: Dependency task ID
            group_id: Group ID

        Raises:
            FairQueueInvalidTaskError: Always raises with circular dependency message

        """
        raise FairQueueInvalidTaskError(
            detail=(
                f"Circular dependency detected: task {current_task_id} depends on {dep_task_id}, "
                f"which depends on {current_task_id}"
            ),
            group_id=group_id,
            validation_error="circular_dependency",
        )

    async def validate_dependencies(
        self,
        task_id: str,
        depends_on: list[str],
        group_id: str | None,
        storage: Any,  # AbstractFairQueueStorage
    ) -> None:
        """Validate dependencies with DAG cycle detection.

        Args:
            task_id: Task ID being validated
            depends_on: List of task IDs this task depends on
            group_id: Group ID of the task
            storage: Storage backend for checking dependencies

        Raises:
            FairQueueInvalidTaskError: If dependencies are invalid

        """
        if not depends_on:
            return

        # Check for duplicate dependencies
        if len(depends_on) != len(set(depends_on)):
            raise FairQueueInvalidTaskError(
                detail="Duplicate task IDs in depends_on list",
                group_id=group_id,
                validation_error="duplicate_dependencies",
            )

        # Limit dependency chain depth
        if len(depends_on) > self._max_depth:
            raise FairQueueInvalidTaskError(
                detail=f"Too many dependencies (max {self._max_depth}, got {len(depends_on)})",
                group_id=group_id,
                validation_error="too_many_dependencies",
            )

        # Validate task ID format
        for dep_task_id in depends_on:
            if not dep_task_id or not isinstance(dep_task_id, str):
                raise FairQueueInvalidTaskError(
                    detail=f"Invalid dependency task ID: {dep_task_id}",
                    group_id=group_id,
                    validation_error="invalid_dependency_id",
                )

            if len(dep_task_id) > self._max_task_id_length:
                raise FairQueueInvalidTaskError(
                    detail=f"Dependency task ID is too long: {dep_task_id[:50]}...",
                    group_id=group_id,
                    validation_error="dependency_id_too_long",
                )

            # Check for direct circular dependency
            if dep_task_id == task_id:
                raise FairQueueInvalidTaskError(
                    detail=f"Circular dependency detected: task {task_id} depends on itself",
                    group_id=group_id,
                    validation_error="circular_dependency",
                )

        # Perform DAG validation for circular dependencies
        await self._validate_dag_no_cycles(task_id, depends_on, group_id, storage)

    async def _validate_dag_no_cycles(
        self,
        current_task_id: str,
        depends_on: list[str],
        group_id: str | None,
        storage: Any,
        visited: set[str] | None = None,
    ) -> None:
        """Validate that adding this dependency doesn't create a cycle in the DAG.

        Args:
            current_task_id: Task ID being enqueued
            depends_on: List of task IDs this task depends on
            group_id: Group ID of the task
            storage: Storage backend
            visited: Set of visited task IDs (for recursion)

        Raises:
            FairQueueInvalidTaskError: If a cycle is detected

        """
        if visited is None:
            visited = set()

        # Check if current task would create a cycle
        if current_task_id in visited:
            cycle_path = [*visited, current_task_id]
            raise FairQueueInvalidTaskError(
                detail=f"Circular dependency detected: {' -> '.join(cycle_path)} -> {current_task_id}",
                group_id=group_id,
                validation_error="circular_dependency",
            )

        visited.add(current_task_id)

        # Recursively check all dependencies
        for dep_task_id in depends_on:
            try:
                dep_task = await storage.get_task(dep_task_id, "")
                if dep_task and dep_task.depends_on:
                    # Check if this dependency would create a cycle back to current_task_id
                    if current_task_id in dep_task.depends_on:
                        self._raise_circular_dependency_error(current_task_id, dep_task_id, group_id)
                    # Recursively check dependencies
                    await self._validate_dag_no_cycles(
                        dep_task_id, dep_task.depends_on, group_id, storage, visited.copy()
                    )
            except FairQueueInvalidTaskError:
                # Re-raise validation errors
                raise
            except Exception:  # noqa: S110
                # If we can't fetch the dependency, assume it's valid
                # (it might not exist yet, which is fine for forward references)
                # This is intentional - we don't want to fail on transient storage errors
                pass


class DefaultTaskPrioritizer:
    """Default task prioritizer using simple priority field."""

    def calculate_priority(
        self,
        task: FairTask,
        group_state: GroupQueueState | None,  # noqa: ARG002
    ) -> int:
        """Calculate effective priority (simply returns task.priority).

        Args:
            task: Task to prioritize
            group_state: Current state of the task's group queue (unused in default)

        Returns:
            Task priority value

        """
        return task.priority


class DefaultGroupWeightCalculator:
    """Default group weight calculator using fixed weight."""

    def calculate_weight(
        self,
        group_id: str,  # noqa: ARG002
        group_state: GroupQueueState,
    ) -> float:
        """Calculate weight (returns group_state.weight).

        Args:
            group_id: Group ID (unused in default)
            group_state: Current state of the group's queue

        Returns:
            Group weight value

        """
        return group_state.weight
