"""cortex/api/routes/commitments.py — HTTP routes for commitments and open loops (Issue #364).

Endpoints:
    POST   /api/v1/commitments          — create commitment
    GET    /api/v1/commitments          — list (filter by owner, status, due)
    PATCH  /api/v1/commitments/{id}     — close/update commitment
    POST   /api/v1/open-loops           — create open loop
    GET    /api/v1/open-loops           — list open loops
    PATCH  /api/v1/open-loops/{id}      — resolve open loop
"""

from __future__ import annotations

import logging
from typing import Any, Optional

from fastapi import APIRouter, Depends, HTTPException, Query, status
from pydantic import BaseModel, field_validator

from cortex.api.deps import PREFIX, get_plugin
from cortex.commitments.models import Commitment, OpenLoop
from cortex.commitments.store import (
    close_commitment,
    create_commitment,
    create_open_loop,
    list_commitments,
    list_open_loops,
    resolve_open_loop,
)

logger = logging.getLogger(__name__)

router = APIRouter()


# ---------------------------------------------------------------------------
# Request / Response models
# ---------------------------------------------------------------------------

class CommitmentCreateRequest(BaseModel):
    owner_id: str = "default"
    content: str
    due_at: Optional[str] = None
    status: str = Commitment.OPEN
    source_turn_id: Optional[str] = None

    @field_validator("status")
    @classmethod
    def _validate_status(cls, v: str) -> str:
        if v not in Commitment.VALID_STATUSES:
            raise ValueError(f"status must be one of {sorted(Commitment.VALID_STATUSES)}")
        return v


class CommitmentPatchRequest(BaseModel):
    status: str
    owner_id: Optional[str] = None

    @field_validator("status")
    @classmethod
    def _validate_status(cls, v: str) -> str:
        allowed = {Commitment.COMPLETED, Commitment.CANCELLED}
        if v not in allowed:
            raise ValueError(f"PATCH status must be one of {sorted(allowed)}")
        return v


class OpenLoopCreateRequest(BaseModel):
    owner_id: str = "default"
    content: str
    context: Optional[str] = None
    status: str = OpenLoop.OPEN

    @field_validator("status")
    @classmethod
    def _validate_status(cls, v: str) -> str:
        if v not in OpenLoop.VALID_STATUSES:
            raise ValueError(f"status must be one of {sorted(OpenLoop.VALID_STATUSES)}")
        return v


class OpenLoopPatchRequest(BaseModel):
    owner_id: Optional[str] = None


# ---------------------------------------------------------------------------
# Route factory
# ---------------------------------------------------------------------------

def _make_routes(verify_api_key):
    """Register all commitment/open-loop routes."""

    # ------------------------------------------------------------------ #
    # Commitments
    # ------------------------------------------------------------------ #

    @router.post(
        f"{PREFIX}/commitments",
        status_code=status.HTTP_201_CREATED,
        summary="Create a commitment",
        tags=["commitments"],
    )
    async def create_commitment_route(
        body: CommitmentCreateRequest,
        _: None = Depends(verify_api_key),
        plugin: Any = Depends(get_plugin),
    ):
        """Create a new commitment (explicit obligation tracked across sessions)."""
        try:
            obj = await create_commitment(
                plugin.storage,
                owner_id=body.owner_id,
                content=body.content,
                due_at=body.due_at,
                status=body.status,
                source_turn_id=body.source_turn_id,
            )
        except ValueError as exc:
            raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc))
        return _commitment_to_dict(obj)

    @router.get(
        f"{PREFIX}/commitments",
        summary="List commitments",
        tags=["commitments"],
    )
    async def list_commitments_route(
        owner_id: str = Query("default"),
        commitment_status: Optional[str] = Query(None, alias="status"),
        due_before: Optional[str] = Query(None),
        limit: int = Query(100, ge=1, le=1000),
        offset: int = Query(0, ge=0),
        _: None = Depends(verify_api_key),
        plugin: Any = Depends(get_plugin),
    ):
        """List commitments with optional filters."""
        items = await list_commitments(
            plugin.storage,
            owner_id=owner_id,
            status=commitment_status,
            due_before=due_before,
            limit=limit,
            offset=offset,
        )
        return {"commitments": [_commitment_to_dict(c) for c in items], "total": len(items)}

    @router.patch(
        f"{PREFIX}/commitments/{{commitment_id}}",
        summary="Close or update a commitment",
        tags=["commitments"],
    )
    async def patch_commitment_route(
        commitment_id: str,
        body: CommitmentPatchRequest,
        _: None = Depends(verify_api_key),
        plugin: Any = Depends(get_plugin),
    ):
        """Close a commitment (mark as completed or cancelled)."""
        try:
            obj = await close_commitment(
                plugin.storage,
                commitment_id=commitment_id,
                status=body.status,
                owner_id=body.owner_id,
            )
        except ValueError as exc:
            raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc))

        if obj is None:
            raise HTTPException(
                status_code=status.HTTP_404_NOT_FOUND,
                detail=f"Commitment {commitment_id!r} not found",
            )
        return _commitment_to_dict(obj)

    # ------------------------------------------------------------------ #
    # Open Loops
    # ------------------------------------------------------------------ #

    @router.post(
        f"{PREFIX}/open-loops",
        status_code=status.HTTP_201_CREATED,
        summary="Create an open loop",
        tags=["open-loops"],
    )
    async def create_open_loop_route(
        body: OpenLoopCreateRequest,
        _: None = Depends(verify_api_key),
        plugin: Any = Depends(get_plugin),
    ):
        """Create a new open loop (unresolved thread carried across sleep/wake cycles)."""
        try:
            obj = await create_open_loop(
                plugin.storage,
                owner_id=body.owner_id,
                content=body.content,
                context=body.context,
                status=body.status,
            )
        except ValueError as exc:
            raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc))
        return _open_loop_to_dict(obj)

    @router.get(
        f"{PREFIX}/open-loops",
        summary="List open loops",
        tags=["open-loops"],
    )
    async def list_open_loops_route(
        owner_id: str = Query("default"),
        loop_status: Optional[str] = Query(None, alias="status"),
        limit: int = Query(100, ge=1, le=1000),
        offset: int = Query(0, ge=0),
        _: None = Depends(verify_api_key),
        plugin: Any = Depends(get_plugin),
    ):
        """List open loops with optional status filter."""
        items = await list_open_loops(
            plugin.storage,
            owner_id=owner_id,
            status=loop_status,
            limit=limit,
            offset=offset,
        )
        return {"open_loops": [_open_loop_to_dict(lp) for lp in items], "total": len(items)}

    @router.patch(
        f"{PREFIX}/open-loops/{{loop_id}}",
        summary="Resolve an open loop",
        tags=["open-loops"],
    )
    async def patch_open_loop_route(
        loop_id: str,
        body: OpenLoopPatchRequest,
        _: None = Depends(verify_api_key),
        plugin: Any = Depends(get_plugin),
    ):
        """Resolve an open loop."""
        obj = await resolve_open_loop(
            plugin.storage,
            loop_id=loop_id,
            owner_id=body.owner_id,
        )
        if obj is None:
            raise HTTPException(
                status_code=status.HTTP_404_NOT_FOUND,
                detail=f"OpenLoop {loop_id!r} not found",
            )
        return _open_loop_to_dict(obj)


# ---------------------------------------------------------------------------
# Serialisation helpers
# ---------------------------------------------------------------------------

def _commitment_to_dict(c: Commitment) -> dict:
    return {
        "id": c.id,
        "owner_id": c.owner_id,
        "content": c.content,
        "due_at": c.due_at,
        "status": c.status,
        "source_turn_id": c.source_turn_id,
        "created_at": c.created_at,
        "closed_at": c.closed_at,
    }


def _open_loop_to_dict(lp: OpenLoop) -> dict:
    return {
        "id": lp.id,
        "owner_id": lp.owner_id,
        "content": lp.content,
        "context": lp.context,
        "status": lp.status,
        "carry_count": lp.carry_count,
        "created_at": lp.created_at,
        "resolved_at": lp.resolved_at,
    }
