"""
HTTP server for agent requests.

Provides a REST API for running agents programmatically from external tools,
CI/CD pipelines, or other services.

Usage:
    persuade serve --port 8765

    # Then call from external tool:
    curl -X POST http://localhost:8765/agent/run \\
      -H "Content-Type: application/json" \\
      -d '{"agent_type": "iterate-pr", "params": {"pr_number": 67}}'
"""

from __future__ import annotations

import uuid
from datetime import UTC, datetime
from pathlib import Path
from typing import TYPE_CHECKING, Any, TypedDict

from persuade.agents.base import AgentPhaseResult, TwoPhaseAgent
from persuade.logging import get_logger

logger = get_logger()


class PendingPlan(TypedDict):
    """Structure for pending agent plans awaiting approval."""

    agent: TwoPhaseAgent
    plan_result: AgentPhaseResult
    created_at: str


# Check FastAPI availability
try:
    from fastapi import FastAPI, HTTPException
    from pydantic import BaseModel, Field

    FASTAPI_AVAILABLE = True

    class AgentRequest(BaseModel):
        """Request to run an agent."""

        agent_type: str = Field(..., description="Type of agent to run (e.g., 'iterate-pr')")
        params: dict[str, Any] = Field(default_factory=dict, description="Parameters for the agent")
        auto_approve: bool = Field(
            default=False, description="If true, execute immediately without approval"
        )
        project_dir: str | None = Field(
            default=None, description="Project directory (defaults to server's cwd)"
        )

    class AgentResponse(BaseModel):
        """Response from an agent operation."""

        request_id: str = Field(..., description="Unique ID for this request")
        success: bool = Field(..., description="Whether the operation succeeded")
        phase: str = Field(..., description="Current phase: 'plan' or 'execute'")
        messages: list[str] = Field(default_factory=list, description="Status messages")
        data: dict[str, Any] = Field(default_factory=dict, description="Result data")
        error: str | None = Field(default=None, description="Error message if failed")

    class HealthResponse(BaseModel):
        """Health check response."""

        status: str
        timestamp: str
        agents_available: list[str]
        warnings: list[str] = Field(default_factory=list, description="Production warnings")

    # TTL for pending plans (prevents memory leaks from uncompleted plans)
    PENDING_PLAN_TTL_SECONDS = 3600  # 1 hour
    MAX_PENDING_PLANS = 1000  # Hard limit to prevent unbounded growth

except ImportError:
    FASTAPI_AVAILABLE = False

if TYPE_CHECKING:
    from fastapi import FastAPI


def create_app() -> FastAPI:
    """Create and configure the FastAPI application.

    WARNING: This server uses in-memory storage for pending plans. This means:
    - Plans are lost on server restart
    - Not suitable for multi-instance deployments
    - Plans expire after 1 hour (PENDING_PLAN_TTL_SECONDS)

    For production deployments requiring persistence or high availability,
    implement a custom PlanStorage backend using Redis, PostgreSQL, or
    another shared storage solution.

    Returns:
        Configured FastAPI application

    Raises:
        ImportError: If FastAPI is not installed
    """
    if not FASTAPI_AVAILABLE:
        raise ImportError("FastAPI is not installed. Install with: pip install persuade[server]")

    app = FastAPI(
        title="Persuade Agent Server",
        description="HTTP API for running persuade agents",
        version="1.0.0",
    )

    # In-memory storage for pending plans.
    # NOTE: Single-instance only. Plans are kept in process memory and will be
    # lost on server/process restart. Plans also expire after PENDING_PLAN_TTL_SECONDS.
    # For production, multi-instance deployments, or when you need persistence
    # and higher reliability, replace this with a shared store such as Redis or
    # a database-backed implementation.
    pending_plans: dict[str, PendingPlan] = {}

    def _cleanup_expired_plans() -> None:
        """Remove expired plans and enforce size limit."""
        now = datetime.now(UTC)

        # Remove expired plans (treat malformed timestamps as expired)
        expired = []
        for rid, plan in pending_plans.items():
            try:
                created_at = datetime.fromisoformat(plan["created_at"])
                if (now - created_at).total_seconds() > PENDING_PLAN_TTL_SECONDS:
                    expired.append(rid)
            except (ValueError, KeyError):
                # Malformed or missing timestamp - treat as expired
                expired.append(rid)
                logger.debug(f"Removing plan with invalid timestamp: {rid}")

        for rid in expired:
            pending_plans.pop(rid, None)
            logger.debug(f"Expired pending plan: {rid}")

        # Enforce size limit by removing oldest if over limit
        while len(pending_plans) >= MAX_PENDING_PLANS and pending_plans:
            oldest = min(pending_plans.items(), key=lambda x: x[1]["created_at"])
            pending_plans.pop(oldest[0])
            logger.debug(f"Evicted oldest pending plan due to size limit: {oldest[0]}")

    @app.get("/health", response_model=HealthResponse)
    async def health_check() -> HealthResponse:
        """Health check endpoint."""
        return HealthResponse(
            status="healthy",
            timestamp=datetime.now(UTC).isoformat(),
            agents_available=["iterate-pr"],
            warnings=["In-memory plan storage: plans lost on restart, not for production"],
        )

    @app.post("/agent/run", response_model=AgentResponse)
    async def run_agent(body: AgentRequest) -> AgentResponse:
        """Run an agent.

        If auto_approve is False (default), only runs the plan phase and
        returns a request_id that can be used to execute later.

        If auto_approve is True, runs both plan and execute phases.
        """
        # Clean up expired pending plans to prevent memory leaks
        _cleanup_expired_plans()

        request_id = str(uuid.uuid4())[:8]

        # Create the appropriate agent
        if body.agent_type == "iterate-pr":
            from persuade.agents.iterate_pr import IteratePRAgent

            pr_number = body.params.get("pr_number")
            if not pr_number:
                raise HTTPException(400, "Missing required parameter 'pr_number'.")

            # Validate pr_number is a positive integer
            if not isinstance(pr_number, int) or pr_number <= 0:
                raise HTTPException(
                    400, f"Invalid PR number '{pr_number}': must be a positive integer."
                )

            # TODO: project_dir accepts arbitrary paths without validation.
            # Full path validation requires deployment context (e.g., ALLOWED_BASE_DIR).
            # For now, the agent runs within the user's environment with their permissions.
            # See: https://github.com/stavxyz/persuade/pull/67
            project_dir = Path(body.project_dir) if body.project_dir else None
            agent = IteratePRAgent(pr_number, project_dir=project_dir, strict=False)
        else:
            raise HTTPException(400, f"Unknown agent type '{body.agent_type}'.")

        # Run plan phase
        plan_result = await agent.plan()

        if not plan_result.success:
            return AgentResponse(
                request_id=request_id,
                success=False,
                phase="plan",
                messages=plan_result.messages,
                data=plan_result.data,
                error=plan_result.error,
            )

        if body.auto_approve:
            # Execute immediately
            exec_result = await agent.execute(plan_result)
            return AgentResponse(
                request_id=request_id,
                success=exec_result.success,
                phase="execute",
                messages=exec_result.messages,
                data=exec_result.data,
                error=exec_result.error,
            )
        else:
            # Store plan for later execution
            pending_plans[request_id] = {
                "agent": agent,
                "plan_result": plan_result,
                "created_at": datetime.now(UTC).isoformat(),
            }

            return AgentResponse(
                request_id=request_id,
                success=True,
                phase="plan",
                messages=[
                    *plan_result.messages,
                    f"Plan ready. POST to /agent/{request_id}/execute to proceed.",
                ],
                data=plan_result.data,
            )

    @app.post("/agent/{request_id}/execute", response_model=AgentResponse)
    async def execute_pending(request_id: str) -> AgentResponse:
        """Execute a previously planned agent run."""
        if request_id not in pending_plans:
            raise HTTPException(404, f"No pending plan found for request ID '{request_id}'.")

        pending = pending_plans.pop(request_id)
        agent = pending["agent"]
        plan_result = pending["plan_result"]

        exec_result = await agent.execute(plan_result)

        return AgentResponse(
            request_id=request_id,
            success=exec_result.success,
            phase="execute",
            messages=exec_result.messages,
            data=exec_result.data,
            error=exec_result.error,
        )

    @app.get("/agent/{request_id}", response_model=AgentResponse)
    async def get_pending_plan(request_id: str) -> AgentResponse:
        """Get details of a pending plan."""
        if request_id not in pending_plans:
            raise HTTPException(404, f"No pending plan found for request ID '{request_id}'.")

        pending = pending_plans[request_id]
        plan_result: AgentPhaseResult = pending["plan_result"]

        return AgentResponse(
            request_id=request_id,
            success=True,
            phase="plan",
            messages=plan_result.messages,
            data=plan_result.data,
        )

    @app.delete("/agent/{request_id}")
    async def cancel_pending(request_id: str) -> dict[str, str]:
        """Cancel a pending plan."""
        if request_id not in pending_plans:
            raise HTTPException(404, f"No pending plan found for request ID '{request_id}'.")

        pending_plans.pop(request_id)
        return {"status": "cancelled", "request_id": request_id}

    return app


def get_app() -> FastAPI:
    """Get or create the FastAPI application."""
    return create_app()
