"""Shared Pydantic models for AgentEvalTool."""

from datetime import datetime
from enum import Enum
from typing import Any, Optional

from pydantic import BaseModel, Field, field_validator


class PlatformType(str, Enum):
    AI_DIGITAL_EMPLOYEE = "ai_digital_employee"
    AI_ASSISTANT = "ai_assistant"


class ChannelType(str, Enum):
    TUTU_API = "tutu-api"
    OPENCLAW = "openclaw"
    HTTP = "http"


class TargetStatus(str, Enum):
    ACTIVE = "active"
    INACTIVE = "inactive"
    ERROR = "error"


class CaseType(str, Enum):
    SINGLE = "single"
    MULTI_TURN = "multi_turn"
    DYNAMIC = "dynamic"


class ModelCapability(str, Enum):
    CHAT = "chat"
    EMBEDDING = "embedding"
    MODERATION = "moderation"


class ModelProtocol(str, Enum):
    OPENAI_COMPATIBLE = "openai_compatible"
    ANTHROPIC = "anthropic"
    GOOGLE_GEMINI = "google_gemini"
    DASHSCOPE = "dashscope"


class ModelModality(str, Enum):
    TEXT = "text"
    IMAGE = "image"
    AUDIO = "audio"
    VIDEO = "video"


class ModelPurpose(str, Enum):
    GENERATOR = "generator"
    JUDGE = "judge"
    EMBEDDING = "embedding"
    MODERATION = "moderation"
    ANALYSIS = "analysis"


class EvalTarget(BaseModel):
    """Evaluation target (the agent being evaluated)."""

    id: Optional[str] = None
    name: str
    description: str = ""
    platform: PlatformType = PlatformType.AI_DIGITAL_EMPLOYEE
    channel_type: ChannelType = ChannelType.TUTU_API
    channel_config: dict[str, Any] = Field(default_factory=dict)
    status: TargetStatus = TargetStatus.ACTIVE
    created_at: Optional[datetime] = None
    updated_at: Optional[datetime] = None


class Expectation(BaseModel):
    """Expected behavior for a test case."""

    intent: Optional[str] = None
    keywords_include: list[str] = Field(default_factory=list)
    keywords_exclude: list[str] = Field(default_factory=list)
    response_time_max_ms: Optional[int] = None
    coherence_min_score: Optional[float] = None


class EvalRuleConfig(BaseModel):
    """Configuration for an evaluation rule."""

    type: str
    params: dict[str, Any] = Field(default_factory=dict)
    weight: float = 1.0  # used when rule_logic == "weighted"


class RuleLogic(str, Enum):
    """How to combine multiple rule results for a case."""

    ALL = "all"  # all rules must pass (default)
    ANY = "any"  # at least one rule must pass
    WEIGHTED = "weighted"  # weighted average score >= threshold


class Case(BaseModel):
    """A single evaluation case within a scenario."""

    id: str
    type: CaseType = CaseType.SINGLE
    messages: list[str] = Field(default_factory=list)
    prompt: Optional[str] = None
    turns: int = 3
    expectations: Expectation = Field(default_factory=Expectation)
    eval_rules: list[EvalRuleConfig] = Field(default_factory=list)
    rule_logic: RuleLogic = RuleLogic.ALL
    rule_pass_threshold: float = 0.6  # used when rule_logic == "weighted"

    @field_validator("messages")
    @classmethod
    def messages_not_empty(cls, v: list[str], info) -> list[str]:
        data = info.data
        case_type = data.get("type") if data else None
        if case_type and case_type != CaseType.DYNAMIC and not v:
            raise ValueError("messages must not be empty for non-dynamic cases")
        return v


class Scenario(BaseModel):
    """A collection of evaluation cases."""

    id: Optional[str] = None
    name: str
    description: str = ""
    tags: list[str] = Field(default_factory=list)
    cases: list[Case] = Field(default_factory=list)
    model_bindings: dict[ModelPurpose, str] = Field(default_factory=dict)
    llm_config: Optional[dict[str, Any]] = None
    # 考纲版本，由系统维护（ADR-0001）：API 传入值会被忽略
    version: int = 1
    created_at: Optional[datetime] = None
    updated_at: Optional[datetime] = None

    @field_validator("cases")
    @classmethod
    def cases_not_empty(cls, v: list[Case]) -> list[Case]:
        if not v:
            raise ValueError("scenario must contain at least one case")
        return v


class RunStatus(str, Enum):
    PENDING = "pending"
    RUNNING = "running"
    COMPLETED = "completed"
    FAILED = "failed"


class RunTrigger(str, Enum):
    MANUAL = "manual"
    AI_ASSISTANT = "ai_assistant"
    CLI = "cli"
    CAMPAIGN = "campaign"


class RunError(BaseModel):
    """Unified run-level error: user cancellation vs genuine execution fault."""

    code: str = "error"
    message: str = ""


class CaseOutcomeSummary(BaseModel):
    """Per-case authoritative verdict snapshot stored in the run summary."""

    passed: bool = False
    connectivity: bool = False


class RunSummary(BaseModel):
    """Typed value of ``EvalRun.summary`` — the single interface for its keys.

    All fields are defaulted and unknown keys are preserved so summary dicts
    written by older versions keep parsing (and survive read-modify-write).
    """

    model_config = {"extra": "allow"}

    total_cases: int = 0
    passed_cases: int = 0
    failed_cases: int = 0
    total_rules: int = 0
    passed_rules: int = 0
    # 用例级通过率，含执行失败（ADR-0002）；失败/取消的 run 无此值
    pass_rate: Optional[float] = None
    # 判定型通过率：连通用例从分子分母双双剔除；无判定型用例时为空
    judged_pass_rate: Optional[float] = None
    avg_latency_ms: Optional[float] = None
    case_outcomes: dict[str, CaseOutcomeSummary] = Field(default_factory=dict)
    case_errors: list[dict[str, str]] = Field(default_factory=list)
    model_configs: dict[str, Any] = Field(default_factory=dict)
    error: Optional[RunError] = None

    @field_validator("error", mode="before")
    @classmethod
    def _coerce_legacy_error(cls, v: Any) -> Any:
        if isinstance(v, str):
            return {"code": "error", "message": v}
        return v

    @property
    def is_cancelled(self) -> bool:
        """User-initiated cancellation — excluded from aggregation (ADR-0004)."""
        return self.error is not None and self.error.code == "cancelled_by_user"


class EvalRun(BaseModel):
    """A single evaluation run."""

    # summary 以属性赋值写入（engine/routers），赋值时即校验成 RunSummary
    model_config = {"validate_assignment": True}

    id: Optional[str] = None
    target_id: str
    scenario_id: str
    # 创建时快照的场景考纲版本（ADR-0001）
    scenario_version: int = 1
    # 归属的评估活动（Campaign）；手动/单次运行为空
    campaign_id: Optional[str] = None
    # Campaign 计划条目的持久化身份；历史 Run 保持为空，不做回填。
    campaign_plan_index: Optional[int] = Field(default=None, ge=0)
    campaign_occurrence_index: Optional[int] = Field(default=None, ge=0)
    status: RunStatus = RunStatus.PENDING
    triggered_by: RunTrigger = RunTrigger.MANUAL
    started_at: Optional[datetime] = None
    completed_at: Optional[datetime] = None
    summary: Optional[RunSummary] = None


class CampaignStatus(str, Enum):
    """Lifecycle of an evaluation campaign (评估活动)."""

    PLANNED = "planned"
    RUNNING = "running"
    COMPLETED = "completed"
    CANCELLED = "cancelled"
    FAILED = "failed"


class CampaignPlanEntry(BaseModel):
    """One static plan item: run a scenario N times at a window offset."""

    scenario_id: str
    offset_seconds: int = Field(ge=0)
    count: int = Field(default=1, ge=1)


class SchedulerState(BaseModel):
    """Durable scheduler progress for a campaign — restart-safe (ADR-0003).

    ``spawned_indices`` are the plan entries already派生 into child Runs;
    ``errors`` records entries whose spawn failed (marked spawned to avoid
    infinite retry).
    """

    spawned_indices: list[int] = Field(default_factory=list)
    errors: list[dict[str, Any]] = Field(default_factory=list)


class CampaignSummary(BaseModel):
    """Typed value of ``Campaign.summary`` — mirrors RunSummary's treatment.

    Unknown top-level keys are preserved (extra=allow) so summaries written by
    older versions keep parsing and survive read-modify-write.
    """

    model_config = {"extra": "allow"}

    scheduler: SchedulerState = Field(default_factory=SchedulerState)


class ExplorationSeeds(BaseModel):
    """Seed set (种子集) for exploratory evaluation: seed personas × seed goals.

    The comparability unit for exploratory evaluation — campaigns sharing a
    seed set are comparable across periods. Empty/absent means the campaign
    opts out of exploration.
    """

    personas: list[str] = Field(default_factory=list)
    goals: list[str] = Field(default_factory=list)


class ExplorationBudgetConfig(BaseModel):
    """Per-campaign exploration budget override; unset fields fall back to
    platform defaults at enforcement time."""

    max_sessions: Optional[int] = Field(default=None, gt=0)
    max_turns: Optional[int] = Field(default=None, gt=0)
    min_interval_seconds: Optional[int] = Field(default=None, gt=0)


class Campaign(BaseModel):
    """An evaluation campaign: a service-cycle window over a single target,
    driving many child Runs from a static plan (ADR-0003)."""

    # summary 以属性赋值写入（scheduler loop）；赋值时即校验成 CampaignSummary
    model_config = {"validate_assignment": True}

    id: Optional[str] = None
    name: str
    target_id: str
    window_seconds: int = Field(gt=0)
    time_scale: float = Field(default=1.0, gt=0)
    plan: list[CampaignPlanEntry] = Field(min_length=1)
    status: CampaignStatus = CampaignStatus.PLANNED
    started_at: Optional[datetime] = None
    completed_at: Optional[datetime] = None
    created_at: Optional[datetime] = None
    summary: Optional[CampaignSummary] = None
    analysis_model_config_id: Optional[str] = None
    exploration_seeds: Optional[ExplorationSeeds] = None
    exploration_budget: Optional[ExplorationBudgetConfig] = None
    last_patrolled_at: Optional[datetime] = None


class Turn(BaseModel):
    """A single turn in a conversation during evaluation."""

    id: Optional[str] = None
    run_id: str
    case_id: str
    round_index: int
    sent_message: dict[str, Any] = Field(default_factory=dict)
    sent_at: Optional[datetime] = None
    question_msg_id: Optional[str] = None
    reply: Optional[dict[str, Any]] = None
    received_at: Optional[datetime] = None
    latency_ms: Optional[int] = None


class EvalResult(BaseModel):
    """Result of applying one evaluation rule to one turn."""

    id: Optional[str] = None
    run_id: str
    case_id: str
    turn_id: str
    rule_type: str
    passed: bool
    score: Optional[float] = None
    reason: str = ""
