import json
from datetime import datetime, timedelta
from typing import Literal, Optional, Type

import pytz
from pydantic import BaseModel, Field, model_validator

from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.core.config import settings
from app.db.agenttask_oper import AgentTaskOper
from app.utils.timer import TimerUtils


class UpdateAgentTaskInput(BaseModel):
    """更新 Agent 自主定时任务的输入参数。"""

    task_id: int = Field(..., ge=1, description="ID of the task to update.")
    name: Optional[str] = Field(None, min_length=1, max_length=100)
    content: Optional[str] = Field(None, min_length=1, max_length=10000)
    trigger_type: Optional[Literal["date", "cron"]] = Field(
        None,
        description="New trigger type. Must be provided together with trigger.",
    )
    trigger: Optional[str] = Field(
        None,
        min_length=1,
        max_length=200,
        description="New ISO 8601 date or five-field cron expression.",
    )
    delay_minutes: Optional[int] = Field(
        None,
        ge=1,
        le=525600,
        description=(
            "For a one-time date task expressed as 'in N minutes', provide this instead "
            "of trigger together with trigger_type='date'."
        ),
    )
    enabled: Optional[bool] = Field(
        None,
        description="Set false to pause the task or true to resume it.",
    )

    @model_validator(mode="after")
    def validate_update(self) -> "UpdateAgentTaskInput":
        """校验更新内容和触发参数组合。"""
        if self.name is not None:
            self.name = self.name.strip()
            if not self.name:
                raise ValueError("name 不能只包含空白字符")
        if self.content is not None:
            self.content = self.content.strip()
            if not self.content:
                raise ValueError("content 不能只包含空白字符")
        has_schedule_update = any(
            value is not None
            for value in (self.trigger_type, self.trigger, self.delay_minutes)
        )
        if has_schedule_update:
            if self.trigger_type is None:
                raise ValueError("修改触发配置时必须提供 trigger_type")
            if self.trigger_type == "date":
                if self.delay_minutes is not None:
                    # 保持校验幂等，具体绝对时间在更新调度前只计算一次。
                    self.trigger = None
                elif self.trigger is None:
                    raise ValueError("date 任务必须提供 trigger 或 delay_minutes")
            elif self.trigger is None or self.delay_minutes is not None:
                raise ValueError("cron 任务必须提供 trigger，且不能提供 delay_minutes")
        if all(
            value is None
            for value in (
                self.name,
                self.content,
                self.trigger_type,
                self.enabled,
            )
        ):
            raise ValueError("至少需要提供一个要更新的字段")
        return self


class UpdateAgentTaskTool(MoviePilotTool):
    """修改、暂停或恢复 Agent 自主定时任务。"""

    name: str = "update_agent_task"
    tags: list[str] = [ToolTag.Write, ToolTag.Scheduler, ToolTag.Admin]
    description: str = (
        "Update an autonomous agent task's name, instructions, exact date or cron "
        "trigger, relative delay_minutes, or enabled state. Use enabled=false to pause "
        "and enabled=true to resume."
    )
    args_schema: Type[BaseModel] = UpdateAgentTaskInput
    require_admin: bool = True

    def get_tool_message(self, **kwargs: object) -> Optional[str]:
        """生成更新定时任务的提示消息。"""
        return f"更新自主定时任务：{kwargs.get('task_id', '')}"

    def _update_task(self, payload: UpdateAgentTaskInput) -> Optional[dict]:
        """更新当前用户的任务并刷新运行时调度。"""
        from app.scheduler import Scheduler

        oper = AgentTaskOper()
        task = oper.get(task_id=payload.task_id, user_id=str(self._user_id))
        if not task:
            return None
        if task.last_status == "running":
            return {"error": f"Agent 定时任务 {payload.task_id} 正在执行，请稍后再修改"}

        trigger_type = payload.trigger_type or task.trigger_type
        trigger_value = payload.trigger
        if trigger_type == "date" and payload.delay_minutes is not None:
            timezone = pytz.timezone(settings.TZ)
            trigger_value = (
                datetime.now(timezone) + timedelta(minutes=payload.delay_minutes)
            ).isoformat(timespec="seconds")
        if trigger_value is None:
            trigger_value = (
                task.cron_expression if trigger_type == "cron" else task.run_at
            )
        enabled = task.enabled if payload.enabled is None else payload.enabled
        normalized_type, normalized_trigger = TimerUtils.normalize_schedule_trigger(
            trigger_type=trigger_type,
            trigger_value=trigger_value,
            timezone_name=settings.TZ,
            require_future=bool(enabled and trigger_type == "date"),
        )

        update_payload = {}
        if payload.name is not None:
            update_payload["name"] = payload.name.strip()
        if payload.content is not None:
            update_payload["content"] = payload.content.strip()
        if payload.trigger_type is not None:
            update_payload.update(
                {
                    "trigger_type": normalized_type,
                    "cron_expression": (
                        normalized_trigger if normalized_type == "cron" else None
                    ),
                    "run_at": normalized_trigger if normalized_type == "date" else None,
                    "last_status": "waiting",
                    "last_result": None,
                }
            )
        if payload.enabled is not None:
            update_payload["enabled"] = payload.enabled
            if payload.enabled:
                update_payload["last_status"] = "waiting"

        oper.update(
            task_id=payload.task_id,
            payload=update_payload,
            user_id=str(self._user_id),
        )
        scheduler = Scheduler()
        next_run_at = scheduler.update_agent_task_job(payload.task_id)
        updated_task = oper.get(task_id=payload.task_id, user_id=str(self._user_id))
        return oper.to_dict(
            updated_task,
            next_run_at=next_run_at,
            timezone=settings.TZ,
        )

    async def run(
            self,
            task_id: int,
            name: Optional[str] = None,
            content: Optional[str] = None,
            trigger_type: Optional[str] = None,
            trigger: Optional[str] = None,
            delay_minutes: Optional[int] = None,
            enabled: Optional[bool] = None,
            **kwargs: object,
    ) -> str:
        """更新 Agent 自主定时任务。"""
        payload = UpdateAgentTaskInput(
            task_id=task_id,
            name=name,
            content=content,
            trigger_type=trigger_type,
            trigger=trigger,
            delay_minutes=delay_minutes,
            enabled=enabled,
        )
        task = await self.run_blocking("db", self._update_task, payload)
        if not task:
            return f"Agent 定时任务 {task_id} 不存在或不属于当前用户"
        if task.get("error"):
            return task["error"]
        return json.dumps(task, ensure_ascii=False, indent=2)
