"""FastAPI server for the Harbor Viewer."""

import json
import math
import shutil
from pathlib import Path
from typing import Any

from fastapi import FastAPI, HTTPException, Query
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, PlainTextResponse
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel

from harbor.models.job.config import JobConfig
from harbor.models.trial.result import TrialResult
from harbor.viewer.models import (
    FileInfo,
    JobSummary,
    PaginatedResponse,
    TaskSummary,
    TrialSummary,
)
from harbor.viewer.scanner import JobScanner


class SummarizeRequest(BaseModel):
    """Request body for job summarization."""

    model: str = "haiku"
    n_concurrent: int = 32
    only_failed: bool = True


# Maximum file size to serve (1MB)
MAX_FILE_SIZE = 1024 * 1024


def create_app(jobs_dir: Path, static_dir: Path | None = None) -> FastAPI:
    """Create the FastAPI application with routes configured for the given jobs directory.

    Args:
        jobs_dir: Directory containing job/trial data
        static_dir: Optional directory containing static viewer files (index.html, assets/)
    """
    app = FastAPI(
        title="Harbor Viewer",
        description="API for browsing Harbor jobs and trials",
        version="0.1.0",
    )

    # Allow CORS for local development
    app.add_middleware(
        CORSMiddleware,  # type: ignore[arg-type]
        allow_origins=["*"],
        allow_credentials=True,
        allow_methods=["*"],
        allow_headers=["*"],
    )

    scanner = JobScanner(jobs_dir)

    @app.get("/api/jobs", response_model=list[JobSummary])
    def list_jobs() -> list[JobSummary]:
        """List all jobs with summary information."""
        job_names = scanner.list_jobs()
        summaries = []

        for name in job_names:
            result = scanner.get_job_result(name)
            if result:
                summaries.append(
                    JobSummary(
                        name=name,
                        id=result.id,
                        started_at=result.started_at,
                        finished_at=result.finished_at,
                        n_total_trials=result.n_total_trials,
                        n_completed_trials=result.stats.n_trials,
                        n_errors=result.stats.n_errors,
                    )
                )
            else:
                summaries.append(JobSummary(name=name))

        return summaries

    @app.get("/api/jobs/{job_name}")
    def get_job(job_name: str) -> dict[str, Any]:
        """Get full job result details."""
        job_dir = jobs_dir / job_name
        if not job_dir.exists():
            raise HTTPException(status_code=404, detail=f"Job '{job_name}' not found")

        result = scanner.get_job_result(job_name)
        if result is None:
            # Return minimal info for jobs without result.json (incomplete jobs)
            # Count trials from subdirectories
            n_trials = sum(1 for d in job_dir.iterdir() if d.is_dir())
            return {
                "id": job_name,
                "started_at": None,
                "finished_at": None,
                "n_total_trials": n_trials,
                "stats": {"n_trials": 0, "n_errors": 0},
                "job_uri": job_dir.resolve().as_uri(),
            }

        # Convert to dict and add job_uri
        result_dict = result.model_dump(mode="json")
        result_dict["job_uri"] = job_dir.resolve().as_uri()
        return result_dict

    @app.get("/api/jobs/{job_name}/summary")
    def get_job_summary(job_name: str) -> dict[str, str | None]:
        """Get job summary (summary.md file at job root)."""
        job_dir = jobs_dir / job_name
        if not job_dir.exists():
            raise HTTPException(status_code=404, detail=f"Job '{job_name}' not found")

        summary_path = job_dir / "summary.md"
        if summary_path.exists():
            try:
                return {"summary": summary_path.read_text()}
            except Exception:
                return {"summary": "[Error reading file]"}
        return {"summary": None}

    @app.post("/api/jobs/{job_name}/summarize")
    async def summarize_job(
        job_name: str, request: SummarizeRequest
    ) -> dict[str, str | None]:
        """Generate a summary for a job using Claude."""
        job_dir = jobs_dir / job_name
        if not job_dir.exists():
            raise HTTPException(status_code=404, detail=f"Job '{job_name}' not found")

        # Import here to avoid loading heavy dependencies at startup
        from harbor.cli.summarize.summarizer import Summarizer

        summarizer = Summarizer(
            job_dir=job_dir,
            n_concurrent=request.n_concurrent,
            model=request.model,
            only_failed=request.only_failed,
        )

        await summarizer.summarize_async()

        # Read and return the generated summary
        summary_path = job_dir / "summary.md"
        if summary_path.exists():
            try:
                return {"summary": summary_path.read_text()}
            except Exception:
                return {"summary": "[Error reading file]"}
        return {"summary": None}

    @app.delete("/api/jobs/{job_name}")
    def delete_job(job_name: str) -> dict[str, str]:
        """Delete a job and all its trials."""
        job_dir = jobs_dir / job_name
        if not job_dir.exists():
            raise HTTPException(status_code=404, detail=f"Job '{job_name}' not found")

        try:
            shutil.rmtree(job_dir)
            return {"status": "ok", "message": f"Job '{job_name}' deleted"}
        except Exception as e:
            raise HTTPException(
                status_code=500, detail=f"Failed to delete job: {str(e)}"
            )

    @app.get("/api/jobs/{job_name}/config", response_model=JobConfig)
    def get_job_config(job_name: str) -> JobConfig:
        """Get job configuration."""
        config = scanner.get_job_config(job_name)
        if not config:
            raise HTTPException(
                status_code=404, detail=f"Config for job '{job_name}' not found"
            )
        return config

    @app.get("/api/jobs/{job_name}/tasks", response_model=list[TaskSummary])
    def list_tasks(job_name: str) -> list[TaskSummary]:
        """List tasks in a job, grouped by agent + model + source + task_name."""
        trial_names = scanner.list_trials(job_name)
        if not trial_names:
            if job_name not in scanner.list_jobs():
                raise HTTPException(
                    status_code=404, detail=f"Job '{job_name}' not found"
                )
            return []

        # Group trials by (agent_name, model_provider, model_name, source, task_name)
        groups: dict[
            tuple[str | None, str | None, str | None, str | None, str],
            dict[str, float | int],
        ] = {}

        for name in trial_names:
            result = scanner.get_trial_result(job_name, name)
            if not result:
                continue

            agent_name = result.agent_info.name
            model_info = result.agent_info.model_info
            model_name = model_info.name if model_info else None
            model_provider = model_info.provider if model_info else None
            source = result.source
            task_name = result.task_name

            key = (
                agent_name,
                model_provider,
                model_name,
                source,
                task_name,
            )

            if key not in groups:
                groups[key] = {
                    "n_trials": 0,
                    "n_completed": 0,
                    "n_errors": 0,
                    "total_reward": 0.0,
                    "reward_count": 0,
                }

            groups[key]["n_trials"] += 1

            if result.finished_at:
                groups[key]["n_completed"] += 1

            if result.exception_info:
                groups[key]["n_errors"] += 1

            if result.verifier_result and result.verifier_result.rewards:
                reward = result.verifier_result.rewards.get("reward")
                if reward is not None:
                    groups[key]["total_reward"] += reward
                    groups[key]["reward_count"] += 1

        # Convert to TaskSummary list
        summaries = []
        for (
            agent_name,
            model_provider,
            model_name,
            source,
            task_name,
        ), stats in groups.items():
            avg_reward = None
            if stats["reward_count"] > 0:
                avg_reward = stats["total_reward"] / stats["reward_count"]

            summaries.append(
                TaskSummary(
                    task_name=task_name,
                    source=source,
                    agent_name=agent_name,
                    model_provider=model_provider,
                    model_name=model_name,
                    n_trials=int(stats["n_trials"]),
                    n_completed=int(stats["n_completed"]),
                    n_errors=int(stats["n_errors"]),
                    avg_reward=avg_reward,
                )
            )

        return summaries

    @app.get(
        "/api/jobs/{job_name}/trials",
        response_model=PaginatedResponse[TrialSummary],
    )
    def list_trials(
        job_name: str,
        task_name: str | None = Query(default=None, description="Filter by task name"),
        source: str | None = Query(
            default=None, description="Filter by source/dataset"
        ),
        agent_name: str | None = Query(
            default=None, description="Filter by agent name"
        ),
        model_name: str | None = Query(
            default=None, description="Filter by model name"
        ),
        page: int = Query(default=1, ge=1, description="Page number (1-indexed)"),
        page_size: int = Query(
            default=100, ge=1, le=100, description="Number of items per page"
        ),
    ) -> PaginatedResponse[TrialSummary]:
        """List trials in a job with pagination and optional filtering."""
        trial_names = scanner.list_trials(job_name)
        if not trial_names:
            if job_name not in scanner.list_jobs():
                raise HTTPException(
                    status_code=404, detail=f"Job '{job_name}' not found"
                )
            return PaginatedResponse(
                items=[], total=0, page=page, page_size=page_size, total_pages=0
            )

        # Build list of trial summaries with filtering
        all_summaries = []
        for name in trial_names:
            result = scanner.get_trial_result(job_name, name)
            if not result:
                continue

            # Apply filters
            if task_name is not None and result.task_name != task_name:
                continue
            if source is not None and result.source != source:
                continue
            if agent_name is not None and result.agent_info.name != agent_name:
                continue
            model_info = result.agent_info.model_info
            # Build full model name (provider/name) to match frontend format
            if model_info and model_info.provider:
                result_full_model_name = f"{model_info.provider}/{model_info.name}"
            elif model_info:
                result_full_model_name = model_info.name
            else:
                result_full_model_name = None
            if model_name is not None and result_full_model_name != model_name:
                continue

            # Extract primary reward if available
            reward = None
            if result.verifier_result and result.verifier_result.rewards:
                reward = result.verifier_result.rewards.get("reward")

            result_model_provider = model_info.provider if model_info else None
            result_model_name = model_info.name if model_info else None

            all_summaries.append(
                TrialSummary(
                    name=name,
                    task_name=result.task_name,
                    id=result.id,
                    source=result.source,
                    agent_name=result.agent_info.name,
                    model_provider=result_model_provider,
                    model_name=result_model_name,
                    reward=reward,
                    error_type=(
                        result.exception_info.exception_type
                        if result.exception_info
                        else None
                    ),
                    started_at=result.started_at,
                    finished_at=result.finished_at,
                )
            )

        # Paginate
        total = len(all_summaries)
        total_pages = math.ceil(total / page_size) if total > 0 else 0
        start_idx = (page - 1) * page_size
        end_idx = start_idx + page_size
        page_summaries = all_summaries[start_idx:end_idx]

        return PaginatedResponse(
            items=page_summaries,
            total=total,
            page=page,
            page_size=page_size,
            total_pages=total_pages,
        )

    @app.get("/api/jobs/{job_name}/trials/{trial_name}", response_model=TrialResult)
    def get_trial(job_name: str, trial_name: str) -> TrialResult:
        """Get full trial result details."""
        result = scanner.get_trial_result(job_name, trial_name)
        if not result:
            raise HTTPException(
                status_code=404,
                detail=f"Trial '{trial_name}' not found in job '{job_name}'",
            )
        return result

    @app.get("/api/jobs/{job_name}/trials/{trial_name}/trajectory")
    def get_trajectory(job_name: str, trial_name: str) -> dict[str, Any] | None:
        """Get trajectory.json content for a trial."""
        trial_dir = jobs_dir / job_name / trial_name
        if not trial_dir.exists():
            raise HTTPException(
                status_code=404,
                detail=f"Trial '{trial_name}' not found in job '{job_name}'",
            )

        trajectory_path = trial_dir / "agent" / "trajectory.json"
        if not trajectory_path.exists():
            return None

        try:
            return json.loads(trajectory_path.read_text())
        except json.JSONDecodeError:
            raise HTTPException(
                status_code=500, detail="Failed to parse trajectory.json"
            )

    @app.get("/api/jobs/{job_name}/trials/{trial_name}/verifier-output")
    def get_verifier_output(job_name: str, trial_name: str) -> dict[str, str | None]:
        """Get verifier output (test-stdout.txt, test-stderr.txt, and ctrf.json)."""
        trial_dir = jobs_dir / job_name / trial_name
        if not trial_dir.exists():
            raise HTTPException(
                status_code=404,
                detail=f"Trial '{trial_name}' not found in job '{job_name}'",
            )

        verifier_dir = trial_dir / "verifier"
        stdout_path = verifier_dir / "test-stdout.txt"
        stderr_path = verifier_dir / "test-stderr.txt"
        ctrf_path = verifier_dir / "ctrf.json"

        stdout = None
        stderr = None
        ctrf = None

        if stdout_path.exists():
            try:
                stdout = stdout_path.read_text()
            except Exception:
                stdout = "[Error reading file]"

        if stderr_path.exists():
            try:
                stderr = stderr_path.read_text()
            except Exception:
                stderr = "[Error reading file]"

        if ctrf_path.exists():
            try:
                ctrf = ctrf_path.read_text()
            except Exception:
                ctrf = "[Error reading file]"

        return {"stdout": stdout, "stderr": stderr, "ctrf": ctrf}

    @app.get("/api/jobs/{job_name}/trials/{trial_name}/files")
    def list_trial_files(job_name: str, trial_name: str) -> list[FileInfo]:
        """List all files in a trial directory."""
        trial_dir = jobs_dir / job_name / trial_name
        if not trial_dir.exists():
            raise HTTPException(
                status_code=404,
                detail=f"Trial '{trial_name}' not found in job '{job_name}'",
            )

        files: list[FileInfo] = []

        def scan_dir(dir_path: Path, relative_base: str = "") -> None:
            try:
                for item in sorted(dir_path.iterdir()):
                    relative_path = (
                        f"{relative_base}/{item.name}" if relative_base else item.name
                    )
                    if item.is_dir():
                        files.append(
                            FileInfo(
                                path=relative_path,
                                name=item.name,
                                is_dir=True,
                                size=None,
                            )
                        )
                        scan_dir(item, relative_path)
                    else:
                        files.append(
                            FileInfo(
                                path=relative_path,
                                name=item.name,
                                is_dir=False,
                                size=item.stat().st_size,
                            )
                        )
            except PermissionError:
                pass

        scan_dir(trial_dir)
        return files

    @app.get("/api/jobs/{job_name}/trials/{trial_name}/files/{file_path:path}")
    def get_trial_file(
        job_name: str, trial_name: str, file_path: str
    ) -> PlainTextResponse:
        """Get content of a file in a trial directory."""
        trial_dir = jobs_dir / job_name / trial_name
        if not trial_dir.exists():
            raise HTTPException(
                status_code=404,
                detail=f"Trial '{trial_name}' not found in job '{job_name}'",
            )

        # Resolve the path and ensure it's within the trial directory (prevent traversal)
        try:
            full_path = (trial_dir / file_path).resolve()
            if not str(full_path).startswith(str(trial_dir.resolve())):
                raise HTTPException(status_code=403, detail="Access denied")
        except Exception:
            raise HTTPException(status_code=400, detail="Invalid file path")

        if not full_path.exists():
            raise HTTPException(status_code=404, detail="File not found")

        if full_path.is_dir():
            raise HTTPException(status_code=400, detail="Cannot read directory")

        # Check file size
        file_size = full_path.stat().st_size
        if file_size > MAX_FILE_SIZE:
            raise HTTPException(
                status_code=413,
                detail=f"File too large ({file_size} bytes, max {MAX_FILE_SIZE})",
            )

        try:
            content = full_path.read_text()
            return PlainTextResponse(content)
        except UnicodeDecodeError:
            raise HTTPException(
                status_code=415, detail="File is binary and cannot be displayed"
            )

    @app.get("/api/jobs/{job_name}/trials/{trial_name}/agent-logs")
    def get_agent_logs(job_name: str, trial_name: str) -> dict[str, Any]:
        """Get agent log files (oracle.txt, setup/stdout.txt, command-*/stdout.txt)."""
        trial_dir = jobs_dir / job_name / trial_name
        if not trial_dir.exists():
            raise HTTPException(
                status_code=404,
                detail=f"Trial '{trial_name}' not found in job '{job_name}'",
            )

        agent_dir = trial_dir / "agent"
        logs: dict[str, Any] = {
            "oracle": None,
            "setup": None,
            "commands": [],
            "summary": None,
        }

        # Read summary.md if it exists
        summary_path = trial_dir / "summary.md"
        if summary_path.exists():
            try:
                logs["summary"] = summary_path.read_text()
            except Exception:
                logs["summary"] = "[Error reading file]"

        # Read oracle.txt if it exists
        oracle_path = agent_dir / "oracle.txt"
        if oracle_path.exists():
            try:
                logs["oracle"] = oracle_path.read_text()
            except Exception:
                logs["oracle"] = "[Error reading file]"

        # Read setup/stdout.txt if it exists
        setup_stdout_path = agent_dir / "setup" / "stdout.txt"
        if setup_stdout_path.exists():
            try:
                logs["setup"] = setup_stdout_path.read_text()
            except Exception:
                logs["setup"] = "[Error reading file]"

        # Read command-*/stdout.txt files
        i = 0
        while True:
            command_dir = agent_dir / f"command-{i}"
            if not command_dir.exists():
                break
            stdout_path = command_dir / "stdout.txt"
            if stdout_path.exists():
                try:
                    logs["commands"].append(
                        {"index": i, "content": stdout_path.read_text()}
                    )
                except Exception:
                    logs["commands"].append(
                        {"index": i, "content": "[Error reading file]"}
                    )
            i += 1

        return logs

    @app.get("/api/health")
    def health_check() -> dict[str, str]:
        """Health check endpoint."""
        return {"status": "ok"}

    # Serve static viewer files if provided
    if static_dir and static_dir.exists():
        assets_dir = static_dir / "assets"
        if assets_dir.exists():
            app.mount(
                "/assets", StaticFiles(directory=assets_dir), name="static_assets"
            )

        @app.get("/favicon.ico")
        def favicon() -> FileResponse:
            """Serve favicon."""
            return FileResponse(static_dir / "favicon.ico")

        @app.get("/{path:path}")
        def serve_spa(path: str) -> FileResponse:
            """Serve the SPA for all non-API routes."""
            return FileResponse(static_dir / "index.html")

    return app
