from __future__ import annotations

import json
import logging
import os
from datetime import UTC, datetime
from typing import Any

from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException
from pydantic import BaseModel as PydanticBaseModel
from pydantic import Field
from sqlalchemy.orm import Session

from evalkit.engine import EvaluationEngine
from evalkit.models import Suite, TestCase
from evalkit.server.comparison import build_comparison, eval_to_comparison_dict
from evalkit.server.db.models import (
    EvalResultModel,
    EvaluationModel,
    EvaluationStatus,
    EvaluationTagModel,
    MetricScoreModel,
    ProjectModel,
)
from evalkit.server.db.session import SessionLocal, get_db
from evalkit.suites import BUILTIN_SUITES

logger = logging.getLogger("evalkit.server")


def _get_default(key: str, fallback: str) -> str:
    return os.environ.get(key, fallback)


class CreateProjectRequest(PydanticBaseModel):
    name: str
    description: str = ""


class CreateProjectResponse(PydanticBaseModel):
    id: int
    name: str
    description: str


class EvaluateRequest(PydanticBaseModel):
    project_id: int
    suite: Suite | None = None
    judge_provider: str | None = None
    judge_model: str | None = None
    api_key: str | None = None
    test_cases: list[dict[str, Any]] = Field(max_length=10000)
    tags: dict[str, str] | None = None


class EvaluateResponse(PydanticBaseModel):
    evaluation_id: int
    status: str


class MetricScoreData(PydanticBaseModel):
    metric_name: str
    score: float
    passed: bool
    reason: str


class ResultData(PydanticBaseModel):
    test_case: dict[str, Any]
    scores: list[MetricScoreData]
    overall_score: float


class UploadResultsRequest(PydanticBaseModel):
    project_id: int
    suite_name: str
    results: list[ResultData]
    average_score: float | None = None
    tags: dict[str, str] | None = None


class ServerConfigResponse(PydanticBaseModel):
    judge_provider: str
    judge_model: str | None
    suite: str
    available_suites: list[Suite]
    available_providers: list[str]


class EvaluationStatusResponse(PydanticBaseModel):
    id: int
    status: str
    suite_name: str
    average_score: float | None
    created_at: str
    completed_at: str | None
    tags: dict[str, str]


class UpdateTagsRequest(PydanticBaseModel):
    tags: dict[str, str | None]


class UpdateTagsResponse(PydanticBaseModel):
    tags: dict[str, str]


def _save_tags(db: Session, evaluation_id: int, tags: dict[str, str]) -> None:
    for key, value in tags.items():
        db.add(EvaluationTagModel(evaluation_id=evaluation_id, key=key, value=value))


def _get_tags_dict(evaluation: EvaluationModel) -> dict[str, str]:
    return {t.key: t.value for t in evaluation.tags}


router = APIRouter()


@router.get("/config", response_model=ServerConfigResponse)
def get_config():
    return ServerConfigResponse(
        judge_provider=_get_default("EVALKIT_JUDGE_PROVIDER", "openai"),
        judge_model=os.environ.get("EVALKIT_JUDGE_MODEL"),
        suite=_get_default("EVALKIT_SUITE", Suite.RAG_QA.value),
        available_suites=list(BUILTIN_SUITES.keys()),
        available_providers=["openai", "azure", "anthropic", "ollama"],
    )


@router.get("/projects", response_model=list[CreateProjectResponse])
def list_projects(db: Session = Depends(get_db)):
    projects = db.query(ProjectModel).order_by(ProjectModel.created_at.desc()).all()
    return [CreateProjectResponse(id=p.id, name=p.name, description=p.description) for p in projects]


@router.post("/projects", response_model=CreateProjectResponse, status_code=201)
def create_project(request: CreateProjectRequest, db: Session = Depends(get_db)):
    existing = db.query(ProjectModel).filter(ProjectModel.name == request.name).first()
    if existing:
        raise HTTPException(status_code=409, detail=f"Project '{request.name}' already exists")
    project = ProjectModel(name=request.name, description=request.description)
    db.add(project)
    db.commit()
    db.refresh(project)
    return CreateProjectResponse(id=project.id, name=project.name, description=project.description)


@router.post("/evaluate", response_model=EvaluateResponse, status_code=202)
async def evaluate(
    request: EvaluateRequest,
    background_tasks: BackgroundTasks,
    db: Session = Depends(get_db),
):
    project = db.get(ProjectModel, request.project_id)
    if project is None:
        raise HTTPException(status_code=404, detail="Project not found")

    if request.suite is None:
        request.suite = Suite(_get_default("EVALKIT_SUITE", Suite.RAG_QA.value))
    if request.judge_provider is None:
        request.judge_provider = _get_default("EVALKIT_JUDGE_PROVIDER", "openai")
    if request.judge_model is None:
        request.judge_model = os.environ.get("EVALKIT_JUDGE_MODEL")

    evaluation = EvaluationModel(
        project_id=request.project_id,
        suite_name=request.suite.value,
        status=EvaluationStatus.pending,
    )
    db.add(evaluation)
    db.commit()
    db.refresh(evaluation)

    # Auto-tags (server-side)
    auto_tags = {"suite": request.suite.value}
    if request.judge_provider:
        auto_tags["judge_provider"] = request.judge_provider
    if request.judge_model:
        auto_tags["judge_model"] = request.judge_model
    if request.tags:
        auto_tags.update(request.tags)
    _save_tags(db, evaluation.id, auto_tags)
    db.commit()

    logger.info(
        "Evaluation %d queued: project_id=%d, suite=%s, judge=%s/%s, test_cases=%d",
        evaluation.id,
        request.project_id,
        request.suite,
        request.judge_provider,
        request.judge_model,
        len(request.test_cases),
    )
    background_tasks.add_task(_run_evaluation, evaluation.id, request)

    return EvaluateResponse(evaluation_id=evaluation.id, status="pending")


@router.get("/evaluations/{evaluation_id}", response_model=EvaluationStatusResponse)
def get_evaluation_status(evaluation_id: int, db: Session = Depends(get_db)):
    evaluation = db.get(EvaluationModel, evaluation_id)
    if evaluation is None:
        raise HTTPException(status_code=404, detail="Evaluation not found")
    return {
        "id": evaluation.id,
        "status": evaluation.status.value,
        "suite_name": evaluation.suite_name,
        "average_score": evaluation.average_score,
        "created_at": evaluation.created_at.isoformat(),
        "completed_at": evaluation.completed_at.isoformat() if evaluation.completed_at else None,
        "tags": _get_tags_dict(evaluation),
    }


@router.post("/results", response_model=EvaluateResponse, status_code=201)
def upload_results(request: UploadResultsRequest, db: Session = Depends(get_db)):
    project = db.get(ProjectModel, request.project_id)
    if project is None:
        raise HTTPException(status_code=404, detail="Project not found")

    avg = request.average_score
    if avg is None and request.results:
        avg = sum(r.overall_score for r in request.results) / len(request.results)

    evaluation = EvaluationModel(
        project_id=request.project_id,
        suite_name=request.suite_name,
        status=EvaluationStatus.complete,
        average_score=avg,
        completed_at=datetime.now(UTC),
    )
    db.add(evaluation)
    db.flush()

    auto_tags = {"suite": request.suite_name}
    if request.tags:
        auto_tags.update(request.tags)
    _save_tags(db, evaluation.id, auto_tags)

    for r in request.results:
        eval_result = EvalResultModel(
            evaluation_id=evaluation.id,
            test_case_json=json.dumps(r.test_case),
            overall_score=r.overall_score,
        )
        db.add(eval_result)
        db.flush()
        for ms in r.scores:
            db.add(
                MetricScoreModel(
                    eval_result_id=eval_result.id,
                    metric_name=ms.metric_name,
                    score=ms.score,
                    passed=int(ms.passed),
                    reason=ms.reason,
                )
            )

    db.commit()
    db.refresh(evaluation)
    return EvaluateResponse(evaluation_id=evaluation.id, status="complete")


@router.put("/evaluations/{evaluation_id}/tags", response_model=UpdateTagsResponse)
def update_evaluation_tags(evaluation_id: int, request: UpdateTagsRequest, db: Session = Depends(get_db)):
    evaluation = db.get(EvaluationModel, evaluation_id)
    if evaluation is None:
        raise HTTPException(status_code=404, detail="Evaluation not found")

    existing = {t.key: t for t in evaluation.tags}
    for key, value in request.tags.items():
        if value is None:
            if key in existing:
                db.delete(existing[key])
        elif key in existing:
            existing[key].value = value
        else:
            db.add(EvaluationTagModel(evaluation_id=evaluation_id, key=key, value=value))
    db.commit()
    db.refresh(evaluation)
    return UpdateTagsResponse(tags=_get_tags_dict(evaluation))


@router.get("/projects/{project_id}/compare")
def compare_evaluations(project_id: int, ids: str, baseline: int | None = None, db: Session = Depends(get_db)):
    project = db.get(ProjectModel, project_id)
    if project is None:
        raise HTTPException(status_code=404, detail="Project not found")

    try:
        eval_ids = [int(x.strip()) for x in ids.split(",")]
    except ValueError:
        raise HTTPException(status_code=400, detail="ids must be comma-separated integers")

    if len(eval_ids) < 2:
        raise HTTPException(status_code=400, detail="At least 2 evaluation IDs required")

    evaluations = []
    for eid in eval_ids:
        ev = db.get(EvaluationModel, eid)
        if ev is None:
            raise HTTPException(status_code=404, detail=f"Evaluation {eid} not found")
        if ev.project_id != project_id:
            raise HTTPException(status_code=422, detail=f"Evaluation {eid} does not belong to project {project_id}")
        if ev.status != EvaluationStatus.complete:
            raise HTTPException(status_code=422, detail=f"Evaluation {eid} is not complete (status: {ev.status.value})")
        evaluations.append(ev)

    if baseline is not None and baseline not in eval_ids:
        raise HTTPException(status_code=400, detail="baseline must be one of the provided ids")

    # Default baseline to earliest evaluation
    if baseline is None:
        evaluations.sort(key=lambda e: e.id)
        baseline = evaluations[0].id

    baseline_ev = next(e for e in evaluations if e.id == baseline)
    comparison_evs = [e for e in evaluations if e.id != baseline]

    return build_comparison(
        eval_to_comparison_dict(baseline_ev),
        [eval_to_comparison_dict(e) for e in comparison_evs],
    )


async def _run_evaluation(evaluation_id: int, request: EvaluateRequest):
    from evalkit.judges.anthropic import AnthropicJudge
    from evalkit.judges.ollama import OllamaJudge

    # Judge imports are lazy — these are optional dependencies
    from evalkit.judges.openai import AzureOpenAIJudge, OpenAIJudge

    db = SessionLocal()
    try:
        evaluation = db.get(EvaluationModel, evaluation_id)
        evaluation.status = EvaluationStatus.running
        db.commit()
        logger.info(
            "Evaluation %d running: judge=%s/%s, suite=%s",
            evaluation_id,
            request.judge_provider,
            request.judge_model,
            request.suite,
        )

        test_cases = [TestCase(**tc) for tc in request.test_cases]
        suite_factory = BUILTIN_SUITES.get(request.suite)
        if suite_factory is None:
            raise ValueError(f"Unknown suite: {request.suite}")
        suite_instance = suite_factory()

        judge = None
        if suite_instance.requires_judge:
            if request.judge_provider == "openai":
                judge = OpenAIJudge(model=request.judge_model or "gpt-4o-mini", api_key=request.api_key)
            elif request.judge_provider == "azure":
                azure_endpoint = os.environ.get("AZURE_OPENAI_ENDPOINT")
                judge = AzureOpenAIJudge(
                    model=request.judge_model or "gpt-4o-mini",
                    api_key=request.api_key,
                    azure_endpoint=azure_endpoint,
                )
            elif request.judge_provider == "anthropic":
                judge = AnthropicJudge(
                    model=request.judge_model or "claude-haiku-4-5-20251001", api_key=request.api_key
                )
            else:
                ollama_url = os.environ.get("OLLAMA_HOST", "http://localhost:11434")
                judge = OllamaJudge(model=request.judge_model or "gpt-oss", base_url=ollama_url)

        engine = EvaluationEngine(judge=judge)
        report = await engine.run_async(test_cases=test_cases, suite=suite_instance)

        for result in report.results:
            eval_result = EvalResultModel(
                evaluation_id=evaluation_id,
                test_case_json=result.test_case.model_dump_json(),
                overall_score=result.overall_score,
            )
            db.add(eval_result)
            db.flush()
            for ms in result.scores:
                db.add(
                    MetricScoreModel(
                        eval_result_id=eval_result.id,
                        metric_name=ms.metric_name,
                        score=ms.score,
                        passed=int(ms.passed),
                        reason=ms.reason,
                    )
                )

        evaluation.status = EvaluationStatus.complete
        evaluation.average_score = report.summary.average_score
        evaluation.completed_at = datetime.now(UTC)
        db.commit()
        logger.info(
            "Evaluation %d complete: average_score=%.3f, passed=%d/%d",
            evaluation_id,
            report.summary.average_score,
            report.summary.passed,
            report.summary.total_cases,
        )
    except Exception:
        logger.exception("Evaluation %d failed", evaluation_id)
        if db:
            evaluation = db.get(EvaluationModel, evaluation_id)
            if evaluation:
                evaluation.status = EvaluationStatus.failed
                db.commit()
    finally:
        db.close()
