"""API routes for evaluation campaigns (评估活动).

Creating a campaign starts its durable ``CampaignRuntime`` loop,
which spawns child Runs across the (optionally compressed) service-cycle window
until it finishes. Progress is authoritative in the DB, so detail queries report
the live window position and spawned/completed Run counts, and a campaign can be
cancelled mid-flight.
"""

from fastapi import APIRouter, Body, Depends, HTTPException, Response
from pydantic import BaseModel, Field
from sqlmodel import Session

from agenteval.evaluation.analysis import resolve_analysis_model
from agenteval.evaluation.campaign_lifecycle import CampaignCreateError, CampaignLifecycleError
from agenteval.evaluation.campaign_lifecycle import cancel_campaign as cancel_campaign_lifecycle
from agenteval.evaluation.campaign_lifecycle import create_campaign as create_campaign_lifecycle
from agenteval.evaluation.campaign_read_model import CampaignReadModel
from agenteval.evaluation.campaign_runner import campaign_runtime
from agenteval.evaluation.comparison import ComparisonError, validate_comparison_request
from agenteval.evaluation.intelligence_jobs import enqueue_campaign_analysis, enqueue_campaign_comparison
from agenteval.evaluation.report_render import render_campaign_markdown
from agenteval.models import CampaignPlanEntry, CampaignStatus, ExplorationBudgetConfig, ExplorationSeeds
from agenteval.storage.repository import CampaignRepository
from agenteval.web.deps import get_db

router = APIRouter()


class CreateCampaignRequest(BaseModel):
    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)
    analysis_model_config_id: str | None = None
    exploration_seeds: ExplorationSeeds | None = None
    exploration_budget: ExplorationBudgetConfig | None = None


@router.get("")
async def list_campaigns(session: Session = Depends(get_db)) -> list[dict]:
    return CampaignReadModel(session).list_items()


@router.post("")
async def create_campaign(
    request: CreateCampaignRequest,
    session: Session = Depends(get_db),
) -> dict:
    try:
        campaign = create_campaign_lifecycle(
            session,
            name=request.name,
            target_id=request.target_id,
            window_seconds=request.window_seconds,
            time_scale=request.time_scale,
            plan=request.plan,
            analysis_model_config_id=request.analysis_model_config_id,
            exploration_seeds=request.exploration_seeds,
            exploration_budget=request.exploration_budget,
            launch=campaign_runtime.start,
        )
    except CampaignCreateError as exc:
        raise HTTPException(status_code=exc.status_code, detail=exc.detail) from exc
    return campaign.model_dump()


@router.post("/{campaign_id}/cancel")
async def cancel_campaign(campaign_id: str, session: Session = Depends(get_db)) -> dict:
    try:
        campaign = cancel_campaign_lifecycle(session, campaign_id, stop=campaign_runtime.cancel)
    except CampaignLifecycleError as exc:
        raise HTTPException(status_code=exc.status_code, detail=exc.detail) from exc
    return campaign.model_dump()


@router.get("/{campaign_id}/report")
async def get_campaign_report(campaign_id: str, session: Session = Depends(get_db)) -> dict:
    report = CampaignReadModel(session).report(campaign_id)
    if report is None:
        raise HTTPException(status_code=404, detail="campaign not found")
    return report


@router.get("/{campaign_id}/report/markdown")
async def get_campaign_report_markdown(campaign_id: str, session: Session = Depends(get_db)) -> Response:
    projection = CampaignReadModel(session).markdown_projection(campaign_id)
    if projection is None:
        raise HTTPException(status_code=404, detail="campaign not found")

    md = render_campaign_markdown(
        projection["report"],
        analysis=projection["analysis"],
        comparison=projection["comparison"],
        exploration=projection["exploration"],
        target_name=projection["target_name"],
        scenario_names=projection["scenario_names"],
    )
    return Response(
        content=md,
        media_type="text/markdown; charset=utf-8",
        headers={"Content-Disposition": f'attachment; filename="campaign-report-{campaign_id}.md"'},
    )


@router.get("/{campaign_id}/timeline")
async def get_campaign_timeline(campaign_id: str, session: Session = Depends(get_db)) -> dict:
    timeline = CampaignReadModel(session).timeline(campaign_id)
    if timeline is None:
        raise HTTPException(status_code=404, detail="campaign not found")
    return timeline


@router.get("/{campaign_id}/analysis")
async def get_campaign_analysis(campaign_id: str, session: Session = Depends(get_db)) -> dict:
    analysis = CampaignReadModel(session).analysis(campaign_id)
    if analysis is None:
        raise HTTPException(status_code=404, detail="campaign not found")
    return analysis


@router.post("/{campaign_id}/analysis")
async def trigger_campaign_analysis(campaign_id: str, session: Session = Depends(get_db)) -> dict:
    campaign = CampaignRepository(session).get(campaign_id)
    if not campaign:
        raise HTTPException(status_code=404, detail="campaign not found")
    if campaign.status in (CampaignStatus.PLANNED, CampaignStatus.RUNNING):
        raise HTTPException(status_code=400, detail="活动完成后才能生成智能分析")
    if resolve_analysis_model(campaign, session) is None:
        raise HTTPException(
            status_code=400,
            detail="未配置分析模型：请在模型配置中心将某个 chat 配置设为「分析默认」，或为该活动指定分析模型",
        )
    enqueue_campaign_analysis(campaign_id, triggered_by="manual")
    return {"status": "generating"}


@router.get("/{campaign_id}/comparison")
async def get_campaign_comparison(campaign_id: str, session: Session = Depends(get_db)) -> dict:
    """周期对比（v0.8）：自动基线配对 + 叙述行状态 + 机械 diff（现算，不存储）。

    ``metric_diff`` 对应「生效基线」：已有对比行时为该行记录的基线，
    否则为自动基线；两者都没有时为 null。
    """
    comparison = CampaignReadModel(session).comparison(campaign_id)
    if comparison is None:
        raise HTTPException(status_code=404, detail="campaign not found")
    return comparison


class GenerateComparisonRequest(BaseModel):
    baseline_campaign_id: str | None = None


@router.post("/{campaign_id}/comparison")
async def trigger_campaign_comparison(
    campaign_id: str,
    request: GenerateComparisonRequest = Body(default=None),
    session: Session = Depends(get_db),
) -> dict:
    campaign = CampaignRepository(session).get(campaign_id)
    if not campaign:
        raise HTTPException(status_code=404, detail="campaign not found")

    explicit_baseline_id = request.baseline_campaign_id if request else None
    try:
        baseline = validate_comparison_request(session, campaign, explicit_baseline_id=explicit_baseline_id)
    except ComparisonError as exc:
        raise HTTPException(status_code=400, detail=str(exc))

    enqueue_campaign_comparison(
        campaign_id,
        triggered_by="manual",
        baseline_campaign_id=baseline.id,
        session=session,
    )
    return {"status": "generating"}


@router.get("/{campaign_id}")
async def get_campaign(campaign_id: str, session: Session = Depends(get_db)) -> dict:
    campaign = CampaignReadModel(session).detail(campaign_id)
    if campaign is None:
        raise HTTPException(status_code=404, detail="campaign not found")
    return campaign
