"""HTTP entry points for /v1/watches.

`WatchListCreateView` handles POST (create) + GET (list). `WatchDetailView`
handles GET / PUT / DELETE on `/v1/watches/<id>`. `WatchActionsView` covers
the chain-level `/v1/watches/<id>/actions` (list + add). Per-action ops
(`ActionDetailView` edit/remove, `ActionRunsView` runs) live at
`/v1/actions/<action_id>` — addressed by the action's own id, account-scoped.

The `/v1/watches/<id>/...` views inherit `WatchScopedAPIView` and read
`self.watch` directly ; a missing watch raises `WatchNotFound`, DRF
converts to 404. The `/v1/actions/...` views inherit `ActionScopedAPIView`.
"""

from __future__ import annotations

import logging

from pydantic import ValidationError as PydanticValidationError
from rest_framework import status
from rest_framework.response import Response

from accounts.api import AccountScopedAPIView
from common.api_params import parse_limit, wants_dry_run
from common.pydantic_errors import pydantic_errors_to_drf
from openmagpie_schema.watch import (
    WatchActionInput,
    WatchListResponse,
    build_watch_action_input,
)
from telemetry import events as telemetry_events
from telemetry.constants import Surface

from .api import (
    ActionScopedAPIView,
    WatchActionNotFound,
    WatchScopedAPIView,
    WatchSvcMixin,
)
from .policy import PolicyError
from .registry import KNOWN_KINDS
from .serializers import (
    WatchCreateSerializer,
    WatchSetActiveSerializer,
    watch_action_input_wire,
    watch_action_mutation,
    watch_action_wire,
    watch_mutation,
    watch_view,
    watch_wire,
)
from .services.watches._actions import ConcurrentChainError

logger = logging.getLogger("watches")


def _validate_kind(kind: object) -> Response | None:
    """400 Response if `kind` (the action's top-level discriminator on the
    sub-router request) isn't a known action kind ; None if it's valid.
    The chain-write serializer does the equivalent check per action."""
    if kind not in KNOWN_KINDS:
        return Response(
            {"kind": [f"unknown action kind {kind!r}; known: {sorted(KNOWN_KINDS)}"]},
            status=status.HTTP_400_BAD_REQUEST,
        )
    return None


class WatchListCreateView(WatchSvcMixin, AccountScopedAPIView):
    """POST /v1/watches (create), GET /v1/watches (list)."""

    def post(self, request):
        serializer = WatchCreateSerializer(data=request.data, context={"account_id": request.account_id})
        serializer.is_valid(raise_exception=True)
        d = serializer.validated_data
        actions: list[WatchActionInput] = d["actions"]

        if wants_dry_run(request):
            preview = self.watch_svc.build(name=d["name"], is_active=d["is_active"])
            body = watch_mutation(preview, feed_ids=d["feed_ids"], actions=[], dry_run=True).model_dump(mode="json")
            # Preview the chain from the validated inputs (no rows persisted).
            body["actions"] = [
                watch_action_input_wire(a, rank).model_dump(mode="json") for rank, a in enumerate(actions)
            ]
            body.pop("id", None)
            return Response(body, status=status.HTTP_200_OK)

        try:
            watch = self.watch_svc.create(
                user_id=str(request.user.id),
                name=d["name"],
                is_active=d["is_active"],
                feed_ids=d["feed_ids"],
                actions=actions,
            )
        except PolicyError as exc:
            return Response({"actions": [str(exc)]}, status=status.HTTP_400_BAD_REQUEST)
        except ConcurrentChainError as exc:
            return Response({"detail": str(exc)}, status=status.HTTP_409_CONFLICT)
        # Anonymous telemetry (no-op only when opted out). Emitted from this API seam,
        # not the service, so the quickstart seed isn't counted (quickstart_completed
        # covers the install). No enabled() pre-gate like feed_created: the props are
        # already in memory (no query to skip) and capture() self-gates. Guarded so a
        # hiccup never fails the create.
        with telemetry_events.guard():
            telemetry_events.watch_created(
                action_kinds=[a.kind for a in actions],
                feed_count=len(d["feed_ids"]),
                surface=getattr(request, "surface", Surface.API.value),
            )
        return Response(
            watch_mutation(
                watch,
                feed_ids=self.watch_svc.feed_ids(watch),
                actions=self.watch_svc.initial_actions(watch),
                dry_run=False,
            ).model_dump(mode="json"),
            status=status.HTTP_201_CREATED,
        )

    def get(self, request):
        limit = parse_limit(request)
        after = request.query_params.get("after") or None
        watches = self.watch_svc.list(after=after, limit=limit)
        next_cursor = str(watches[-1].id) if len(watches) == limit else None
        items = [watch_wire(w, feed_ids=self.watch_svc.feed_ids(w)) for w in watches]
        return Response(WatchListResponse(items=items, next_cursor=next_cursor).model_dump(mode="json"))


class WatchDetailView(WatchScopedAPIView):
    """GET / PUT / DELETE /v1/watches/<id>, account-scoped."""

    def get(self, request, watch_id: str):
        return Response(
            watch_view(
                self.watch,
                feed_ids=self.watch_svc.feed_ids(self.watch),
                actions=self.watch_svc.initial_actions(self.watch),
            ).model_dump(mode="json"),
            status=status.HTTP_200_OK,
        )

    def put(self, request, watch_id: str):
        serializer = WatchCreateSerializer(data=request.data, context={"account_id": request.account_id})
        serializer.is_valid(raise_exception=True)
        d = serializer.validated_data
        edit = {"name": d["name"], "is_active": d["is_active"], "feed_ids": d["feed_ids"], "actions": d["actions"]}
        try:
            if wants_dry_run(request):
                body = watch_mutation(self.watch, feed_ids=d["feed_ids"], actions=[], dry_run=True).model_dump(
                    mode="json"
                )
                body["actions"] = [
                    watch_action_input_wire(a, rank).model_dump(mode="json") for rank, a in enumerate(d["actions"])
                ]
                return Response(body, status=status.HTTP_200_OK)
            updated = self.watch_svc.update(self.watch, **edit)
            return Response(
                watch_mutation(
                    updated,
                    feed_ids=self.watch_svc.feed_ids(updated),
                    actions=self.watch_svc.initial_actions(updated),
                    dry_run=False,
                ).model_dump(mode="json"),
                status=status.HTTP_200_OK,
            )
        except PolicyError as exc:
            return Response({"actions": [str(exc)]}, status=status.HTTP_400_BAD_REQUEST)
        except ConcurrentChainError as exc:
            return Response({"detail": str(exc)}, status=status.HTTP_409_CONFLICT)

    def patch(self, request, watch_id: str):
        """Pause/resume: flip is_active only (the trigger pass skips inactive watches).
        Distinct from PUT, which full-replaces the action chain. Deliberately returns
        the detail view (watch_view), NOT the *_mutation envelope POST/PUT use: a
        one-bit toggle has no dry-run, so the mutation/dry_run wrapper doesn't apply."""
        serializer = WatchSetActiveSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        self.watch_svc.set_active(self.watch, is_active=serializer.validated_data["is_active"])
        return Response(
            watch_view(
                self.watch,
                feed_ids=self.watch_svc.feed_ids(self.watch),
                actions=self.watch_svc.initial_actions(self.watch),
            ).model_dump(mode="json"),
            status=status.HTTP_200_OK,
        )

    def delete(self, request, watch_id: str):
        self.watch_svc.delete(self.watch)
        return Response(status=status.HTTP_204_NO_CONTENT)


class WatchActionsView(WatchScopedAPIView):
    """Action chain sub-router on `/v1/watches/<id>/actions`.

    GET  list the watch's initial-path actions (the chain), rank order
    POST add one action (body = {config, rank?}); appends when rank omitted
    """

    def get(self, request, watch_id: str):
        return Response(
            {
                "items": [
                    w.model_dump(mode="json")
                    for a in self.watch_svc.initial_actions(self.watch)
                    if (w := watch_action_wire(a)) is not None
                ]
            },
            status=status.HTTP_200_OK,
        )

    def post(self, request, watch_id: str):
        body = request.data
        if not isinstance(body, dict):
            return Response(
                {"detail": "request body must be a JSON object with a `config`"},
                status=status.HTTP_400_BAD_REQUEST,
            )
        config = body.get("config")
        if not isinstance(config, dict):
            return Response(
                {"config": ["this field is required and must be an object"]}, status=status.HTTP_400_BAD_REQUEST
            )
        rank = body.get("rank")
        if rank is not None and not isinstance(rank, int):
            return Response({"rank": ["must be an integer or omitted"]}, status=status.HTTP_400_BAD_REQUEST)
        kind_err = _validate_kind(body.get("kind"))
        if kind_err is not None:
            return kind_err
        # A committed watch always has an initial path (set at create), so
        # this is the chain's path id directly ; no lazy-create.
        dry_run = wants_dry_run(request)
        try:
            created = self.action_svc.add(
                path_id=self.watch.initial_path_id,
                action=build_watch_action_input(kind=str(body["kind"]), config=config),
                rank=rank,
                dry_run=dry_run,
            )
        except PydanticValidationError as exc:
            return Response({"config": pydantic_errors_to_drf(exc)}, status=status.HTTP_400_BAD_REQUEST)
        except PolicyError as exc:
            return Response({"config": [str(exc)]}, status=status.HTTP_400_BAD_REQUEST)
        except ConcurrentChainError as exc:
            return Response({"detail": str(exc)}, status=status.HTTP_409_CONFLICT)
        code = status.HTTP_200_OK if dry_run else status.HTTP_201_CREATED
        return Response(watch_action_mutation(created, dry_run=dry_run).model_dump(mode="json"), status=code)


class ActionDetailView(ActionScopedAPIView):
    """Per-action ops on `/v1/actions/<action_id>`, addressed by the action's
    own (globally unique) id — account-scoped, the watch/chain derived from
    the action rather than passed in the URL.

    GET    read this action's definition (kind + redacted config + summary)
    PUT    replace this action's config in place (same rank)
    DELETE remove the action and close the rank gap
    """

    def get(self, request, action_id: str):
        # `self.action` is the account-scoped row (404 via WatchActionNotFound).
        # Review path for `magpie watch action get`: the definition only, not
        # its runs/deliveries (those are the audit routes that hang off it).
        # A persisted action CAN carry an unrenderable kind (a removed kind / manual
        # corruption), the case watch_view's list guard skips. 404 like the sibling
        # run-detail view (WatchActionRunNotFound) rather than 500 on the None.
        wire = watch_action_wire(self.action)
        if wire is None:
            raise WatchActionNotFound(action_id)
        return Response(wire.model_dump(mode="json"))

    def put(self, request, action_id: str):
        body = request.data
        if not isinstance(body, dict):
            return Response(
                {"detail": "request body must be a JSON object with a `config`"},
                status=status.HTTP_400_BAD_REQUEST,
            )
        config = body.get("config")
        if not isinstance(config, dict):
            return Response(
                {"config": ["this field is required and must be an object"]}, status=status.HTTP_400_BAD_REQUEST
            )
        kind_err = _validate_kind(body.get("kind"))
        if kind_err is not None:
            return kind_err
        dry_run = wants_dry_run(request)
        try:
            updated = self.action_svc.set_config(
                self.action, spec=build_watch_action_input(kind=str(body["kind"]), config=config), dry_run=dry_run
            )
        except PydanticValidationError as exc:
            return Response({"config": pydantic_errors_to_drf(exc)}, status=status.HTTP_400_BAD_REQUEST)
        except PolicyError as exc:
            return Response({"config": [str(exc)]}, status=status.HTTP_400_BAD_REQUEST)
        return Response(
            watch_action_mutation(updated, dry_run=dry_run).model_dump(mode="json"), status=status.HTTP_200_OK
        )

    def delete(self, request, action_id: str):
        try:
            self.action_svc.remove(self.action)
        except ConcurrentChainError as exc:
            return Response({"detail": str(exc)}, status=status.HTTP_409_CONFLICT)
        return Response(status=status.HTTP_204_NO_CONTENT)
