"""任务分发器 - 通过 Redis Queue 将任务分发给 Agent

与业务系统解耦：通过依赖注入获取 Redis 连接和数据库会话。
"""

import ipaddress
import json
import logging
import uuid
from datetime import datetime, timezone
from typing import Any, Callable, Awaitable
from urllib.parse import urlparse

from agentkit.core.exceptions import (
    NoAvailableAgentError,
    TaskDispatchError,
    TaskNotFoundError,
)
from agentkit.core.protocol import (
    AgentStatus,
    TaskMessage,
    TaskProgress,
    TaskResult,
    TaskStatus,
)

logger = logging.getLogger(__name__)

_PRIVATE_NETWORKS = [
    ipaddress.ip_network("127.0.0.0/8"),
    ipaddress.ip_network("10.0.0.0/8"),
    ipaddress.ip_network("172.16.0.0/12"),
    ipaddress.ip_network("192.168.0.0/16"),
    ipaddress.ip_network("169.254.0.0/16"),
    ipaddress.ip_network("::1/128"),
    ipaddress.ip_network("fc00::/7"),
    ipaddress.ip_network("fe80::/10"),
]


def _validate_callback_url(url: str) -> bool:
    """Validate callback URL to prevent SSRF attacks.

    Rules:
    - Only http/https protocols allowed
    - No localhost or loopback addresses
    - No private/internal IP ranges
    - No link-local addresses

    Returns True if valid, False if should be blocked.
    """
    try:
        parsed = urlparse(url)
    except Exception:
        return False

    if parsed.scheme not in ("http", "https"):
        return False

    hostname = parsed.hostname
    if not hostname:
        return False

    if hostname.lower() in ("localhost", "127.0.0.1", "::1"):
        return False

    try:
        ip = ipaddress.ip_address(hostname)
        for network in _PRIVATE_NETWORKS:
            if ip in network:
                return False
    except ValueError:
        pass

    return True


class TaskDispatcher:
    """任务分发器，通过 Redis Queue 将任务分发给 Agent"""

    def __init__(
        self,
        redis_factory: Callable[[], Awaitable[Any]],
        session_factory: Callable[[], Any],
        agent_model: Any,
        task_model: Any,
        task_log_model: Any,
    ):
        """
        Args:
            redis_factory: 返回 Redis 连接的异步工厂
            session_factory: 返回 async context manager 的工厂，用于获取数据库会话
            agent_model: Agent ORM 模型类
            task_model: AgentTask ORM 模型类
            task_log_model: AgentTaskLog ORM 模型类
        """
        self._redis_factory = redis_factory
        self._session_factory = session_factory
        self._agent_model = agent_model
        self._task_model = task_model
        self._task_log_model = task_log_model

    async def _get_redis(self):
        return await self._redis_factory()

    async def dispatch(
        self,
        task: TaskMessage,
        organization_id: str | None = None,
        created_by: str | None = None,
    ) -> str:
        """分发任务到对应 Agent 的队列，返回 task_id"""
        async with self._session_factory() as db:
            try:
                from sqlalchemy import select

                AgentModel = self._agent_model
                TaskModel = self._task_model

                # 查找目标 Agent
                stmt = select(AgentModel).where(AgentModel.name == task.agent_name)
                result = await db.execute(stmt)
                agent = result.scalar_one_or_none()

                if not agent:
                    raise TaskDispatchError(task.task_id, f"Agent '{task.agent_name}' not found")

                if agent.status != AgentStatus.ONLINE:
                    raise TaskDispatchError(
                        task.task_id,
                        f"Agent '{task.agent_name}' is not online (status={agent.status})",
                    )

                # 写入 agent_tasks 表
                task_id_uuid = uuid.UUID(task.task_id)
                agent_task = TaskModel(
                    id=task_id_uuid,
                    agent_id=agent.id,
                    task_type=task.task_type,
                    status=TaskStatus.PENDING,
                    priority=task.priority,
                    input_data=task.input_data,
                    organization_id=uuid.UUID(organization_id) if organization_id else agent.id,
                    created_by=uuid.UUID(created_by) if created_by else None,
                )
                db.add(agent_task)
                await db.commit()

                # 推送到 Redis Queue
                redis = await self._get_redis()
                queue_key = f"agent:{task.agent_name}:tasks"
                await redis.lpush(queue_key, json.dumps(task.to_dict()))

                logger.info(
                    f"Task {task.task_id} dispatched to agent '{task.agent_name}' "
                    f"(type={task.task_type}, priority={task.priority})"
                )
                return task.task_id

            except TaskDispatchError:
                raise
            except Exception as e:
                await db.rollback()
                logger.error(f"Failed to dispatch task {task.task_id}: {e}")
                raise TaskDispatchError(task.task_id, str(e))

    async def cancel_task(self, task_id: str):
        """取消任务"""
        async with self._session_factory() as db:
            try:
                from sqlalchemy import select

                TaskModel = self._task_model
                task_uuid = uuid.UUID(task_id)
                stmt = select(TaskModel).where(TaskModel.id == task_uuid)
                result = await db.execute(stmt)
                task = result.scalar_one_or_none()

                if not task:
                    raise TaskNotFoundError(task_id)

                if task.status in (TaskStatus.COMPLETED, TaskStatus.FAILED, TaskStatus.CANCELLED):
                    logger.warning(f"Cannot cancel task {task_id} with status '{task.status}'")
                    return

                task.status = TaskStatus.CANCELLED
                task.completed_at = datetime.now(timezone.utc)
                await db.commit()

                await self._write_log(
                    db, task_id=task_id, agent_id=str(task.agent_id),
                    log_level="info", message="Task cancelled by user",
                )
                await db.commit()

                logger.info(f"Task {task_id} cancelled")

            except TaskNotFoundError:
                raise
            except Exception as e:
                await db.rollback()
                logger.error(f"Failed to cancel task {task_id}: {e}")
                raise

    async def get_task_status(self, task_id: str) -> dict:
        """获取任务状态"""
        async with self._session_factory() as db:
            from sqlalchemy import select

            TaskModel = self._task_model
            task_uuid = uuid.UUID(task_id)
            stmt = select(TaskModel).where(TaskModel.id == task_uuid)
            result = await db.execute(stmt)
            task = result.scalar_one_or_none()

            if not task:
                raise TaskNotFoundError(task_id)

            return self._task_to_dict(task)

    async def handle_result(self, result: TaskResult):
        """处理 Agent 返回的结果"""
        async with self._session_factory() as db:
            try:
                from sqlalchemy import select

                TaskModel = self._task_model
                task_uuid = uuid.UUID(result.task_id)
                stmt = select(TaskModel).where(TaskModel.id == task_uuid)
                db_result = await db.execute(stmt)
                task = db_result.scalar_one_or_none()

                if not task:
                    logger.error(f"Task {result.task_id} not found when handling result")
                    return

                task.status = result.status
                task.output_data = result.output_data
                task.error_message = result.error_message
                task.started_at = result.started_at
                task.completed_at = result.completed_at
                await db.commit()

                log_level = "info" if result.status == TaskStatus.COMPLETED else "error"
                log_message = (
                    f"Task {result.status}"
                    if result.status == TaskStatus.COMPLETED
                    else f"Task failed: {result.error_message}"
                )
                await self._write_log(
                    db,
                    task_id=result.task_id,
                    agent_id=str(task.agent_id),
                    log_level=log_level,
                    message=log_message,
                    extra_metadata=result.metrics,
                )
                await db.commit()

                # 触发回调
                if result.output_data and result.output_data.get("callback_url"):
                    await self._trigger_callback(result.output_data["callback_url"], result)

                logger.info(f"Task {result.task_id} result handled (status={result.status})")

            except Exception as e:
                await db.rollback()
                logger.error(f"Failed to handle result for task {result.task_id}: {e}")

    async def handle_progress(self, progress: TaskProgress):
        """处理进度上报"""
        async with self._session_factory() as db:
            try:
                from sqlalchemy import select

                AgentModel = self._agent_model
                stmt = select(AgentModel).where(AgentModel.name == progress.agent_name)
                result = await db.execute(stmt)
                agent = result.scalar_one_or_none()

                if not agent:
                    logger.warning(f"Agent '{progress.agent_name}' not found for progress report")
                    return

                await self._write_log(
                    db,
                    task_id=progress.task_id,
                    agent_id=str(agent.id),
                    log_level="info",
                    message=f"Progress: {progress.progress:.0%} - {progress.message}",
                    extra_metadata={
                        "progress": progress.progress,
                        "updated_at": progress.updated_at.isoformat(),
                    },
                )
                await db.commit()

            except Exception as e:
                await db.rollback()
                logger.error(f"Failed to handle progress for task {progress.task_id}: {e}")

    async def retry_failed_tasks(self, max_retries: int = 3):
        """重试失败的任务"""
        async with self._session_factory() as db:
            try:
                from sqlalchemy import select

                TaskModel = self._task_model
                AgentModel = self._agent_model
                LogModel = self._task_log_model

                stmt = select(TaskModel).where(TaskModel.status == TaskStatus.FAILED)
                result = await db.execute(stmt)
                failed_tasks = result.scalars().all()

                retried = 0
                for task in failed_tasks:
                    log_stmt = select(LogModel).where(
                        LogModel.task_id == task.id,
                        LogModel.message.like("%retry%"),
                    )
                    log_result = await db.execute(log_stmt)
                    retry_count = len(log_result.scalars().all())

                    if retry_count < max_retries:
                        task.status = TaskStatus.PENDING
                        task.error_message = None
                        task.started_at = None
                        task.completed_at = None

                        agent_stmt = select(AgentModel).where(AgentModel.id == task.agent_id)
                        agent_result = await db.execute(agent_stmt)
                        agent = agent_result.scalar_one_or_none()

                        if agent and agent.status == AgentStatus.ONLINE:
                            task_msg = TaskMessage(
                                task_id=str(task.id),
                                agent_name=agent.name,
                                task_type=task.task_type,
                                priority=task.priority,
                                input_data=task.input_data or {},
                                callback_url=None,
                                created_at=datetime.now(timezone.utc),
                            )
                            redis = await self._get_redis()
                            queue_key = f"agent:{agent.name}:tasks"
                            await redis.lpush(queue_key, json.dumps(task_msg.to_dict()))

                            await self._write_log(
                                db,
                                task_id=str(task.id),
                                agent_id=str(agent.id),
                                log_level="info",
                                message=f"Task retry attempt {retry_count + 1}/{max_retries}",
                            )
                            retried += 1

                await db.commit()
                if retried > 0:
                    logger.info(f"Retried {retried} failed tasks")

            except Exception as e:
                await db.rollback()
                logger.error(f"Failed to retry failed tasks: {e}")

    async def _write_log(
        self,
        db: Any,
        task_id: str,
        agent_id: str,
        log_level: str,
        message: str,
        extra_metadata: dict | None = None,
    ):
        LogModel = self._task_log_model
        log_entry = LogModel(
            task_id=uuid.UUID(task_id),
            agent_id=uuid.UUID(agent_id),
            log_level=log_level,
            message=message,
            extra_metadata=extra_metadata,
        )
        db.add(log_entry)

    async def _trigger_callback(self, callback_url: str, result: TaskResult):
        if not _validate_callback_url(callback_url):
            logger.warning(f"Callback URL rejected (SSRF protection): {callback_url}")
            return

        try:
            import httpx
            async with httpx.AsyncClient(timeout=10) as client:
                await client.post(callback_url, json=result.to_dict())
                logger.info(f"Callback triggered for task {result.task_id}")
        except Exception as e:
            logger.warning(f"Callback failed for task {result.task_id}: {e}")

    def _task_to_dict(self, task: Any) -> dict:
        return {
            "id": str(task.id),
            "agent_id": str(task.agent_id),
            "task_type": task.task_type,
            "status": task.status,
            "priority": task.priority,
            "input_data": task.input_data,
            "output_data": task.output_data,
            "error_message": task.error_message,
            "created_by": str(task.created_by) if task.created_by else None,
            "organization_id": str(task.organization_id),
            "project_id": str(task.project_id) if task.project_id else None,
            "scheduled_at": task.scheduled_at.isoformat() if task.scheduled_at else None,
            "started_at": task.started_at.isoformat() if task.started_at else None,
            "completed_at": task.completed_at.isoformat() if task.completed_at else None,
            "created_at": task.created_at.isoformat() if task.created_at else None,
        }
