"""cortex/api/routes/entities.py — Entity + GDPR endpoints.

Canonical GDPR owner-forget path: ``DELETE /api/v1/owners/{owner_id}/forget``
Deprecated path: ``DELETE /api/v1/owner/{owner_id}/forget`` (sunset 2026-06-01, issue #301)

Security fix (#1536): Entity/peer routes now use auth-derived owner_id via
``_resolve_owner_id()`` instead of ``getattr(plugin.config, 'owner_id', ...)``.
"""

from __future__ import annotations

import logging
import sqlite3
from typing import Any, Dict, List, Optional

from fastapi import APIRouter, Depends, HTTPException, Request, Response, status
from fastapi.responses import JSONResponse

from cortex.api.deps import PREFIX, _resolve_owner_id, _serialise, get_plugin
from cortex.api.deprecation import add_deprecation_headers
from cortex.api.models import EntityListResponse, MergeEntityRequest

logger = logging.getLogger(__name__)

router = APIRouter()


def _make_routes(verify_api_key):
    """Register entity routes with the given auth dependency."""

    @router.get(
        f"{PREFIX}/entities",
        response_model=EntityListResponse,
        summary="Browse entities",
        tags=["entities"],
    )
    async def list_entities(
        request: Request,
        entity_type: Optional[str] = None,
        visibility_status: Optional[str] = None,
        sort_by: str = "last_seen",
        page: int = 1,
        per_page: int = 50,
        limit: int = 50,
        offset: int = 0,
        search: Optional[str] = None,
        _: None = Depends(verify_api_key),
        plugin: Any = Depends(get_plugin),
    ) -> EntityListResponse:
        """Browse resolved entities with optional type/visibility filter, sort, and pagination."""
        errors: List[str] = []
        entities: List[Dict[str, Any]] = []
        total = 0

        if plugin.storage is None:
            errors.append("Storage not available")
            return EntityListResponse(errors=errors)

        eff_limit = per_page if per_page != 50 else limit
        eff_offset = (page - 1) * per_page if page > 1 else offset

        effective_owner = _resolve_owner_id(request)
        try:
            if hasattr(plugin.storage, "list_entities"):
                _kwargs: Dict[str, Any] = dict(
                    owner_id=effective_owner,
                    entity_type=entity_type,
                    sort_by=sort_by,
                    limit=eff_limit,
                    offset=eff_offset,
                    search=search,
                )
                if visibility_status is not None:
                    _kwargs["visibility_status"] = visibility_status
                raw = await plugin.storage.list_entities(**_kwargs)
                if hasattr(plugin.storage, "count_entities"):
                    total = await plugin.storage.count_entities(
                        owner_id=effective_owner,
                    )
                else:
                    total = len(raw)

                for ent in raw:
                    d = _serialise(ent.__dict__) if hasattr(ent, "__dict__") else _serialise(ent)
                    if hasattr(plugin.storage, "get_entity_claim_count"):
                        try:
                            d["claim_count"] = await plugin.storage.get_entity_claim_count(d.get("id", ""))
                        except Exception:
                            d["claim_count"] = 0
                    entities.append(d)
            else:
                errors.append("Entity listing not supported by storage")
        except Exception as exc:
            err = f"Entity list failed: {exc}"
            logger.error(err, exc_info=True)
            errors.append(err)

        return EntityListResponse(entities=entities, total=total, errors=errors)

    @router.get(
        f"{PREFIX}/dashboard/entities",
        summary="Dashboard entities (peers + promoted entities)",
        tags=["dashboard"],
    )
    async def dashboard_entities(
        request: Request,
        _: None = Depends(verify_api_key),
        plugin: Any = Depends(get_plugin),
    ) -> JSONResponse:
        """Return combined dashboard entities from the dashboard_entities view.

        NOTE: dashboard_entities is a DB view not currently scoped by owner_id.
        Auth is enforced by verify_api_key; owner available via
        ``_resolve_owner_id(request)`` for future per-owner filtering.
        """
        storage = getattr(plugin, "storage", None)
        if storage is None:
            raise HTTPException(503, detail="Storage not available")
        try:
            # Use public adapter method instead of private _get_conn() (#1541).
            effective_owner = _resolve_owner_id(request)
            try:
                rows = await storage.execute_fetchall(
                    "SELECT * FROM dashboard_entities WHERE owner_id = ?",
                    (effective_owner,),
                )
            except sqlite3.OperationalError:
                # View may not have owner_id column yet — fall back unscoped
                logger.warning(
                    "dashboard_entities view may lack owner_id column — "
                    "falling back to unscoped query"
                )
                rows = await storage.execute_fetchall("SELECT * FROM dashboard_entities")
            items = [dict(r) for r in rows]
            return JSONResponse(content={"entities": items, "total": len(items)})
        except Exception as exc:
            logger.error("dashboard_entities failed: %s", exc, exc_info=True)
            raise HTTPException(500, detail="Internal server error")

    @router.get(
        f"{PREFIX}/entities/{{entity_id}}",
        summary="Entity detail",
        tags=["entities"],
    )
    async def get_entity_detail(
        entity_id: str,
        request: Request,
        _: None = Depends(verify_api_key),
        plugin: Any = Depends(get_plugin),
    ) -> JSONResponse:
        """Return entity info + all aliases + claims + top relationships."""
        storage = getattr(plugin, "storage", None)
        if storage is None:
            raise HTTPException(503, detail="Storage not available")
        try:
            entity = await storage.get_entity(entity_id)
            if entity is None:
                raise HTTPException(404, detail=f"Entity {entity_id} not found")
            data = _serialise(entity.__dict__) if hasattr(entity, "__dict__") else _serialise(entity)

            aliases: List[Dict[str, Any]] = []
            if hasattr(storage, "get_entity_aliases"):
                raw_aliases = await storage.get_entity_aliases(entity_id)
                aliases = [
                    _serialise(a.__dict__) if hasattr(a, "__dict__") else _serialise(a)
                    for a in raw_aliases
                ]
            data["aliases"] = aliases

            claims: List[Dict[str, Any]] = []
            effective_owner = _resolve_owner_id(request)
            if hasattr(storage, "get_claims_by_entity"):
                claim_objs = await storage.get_claims_by_entity(
                    entity_id,
                    owner_id=effective_owner,
                )
                claims = [
                    _serialise(c.__dict__) if hasattr(c, "__dict__") else _serialise(c)
                    for c in claim_objs
                ]
            data["claims"] = claims

            claim_count = len(claims) if claims else 0
            if not claims and hasattr(storage, "get_entity_claim_count"):
                claim_count = await storage.get_entity_claim_count(entity_id)
            data["claim_count"] = claim_count

            relationships: List[Dict[str, Any]] = []
            if hasattr(storage, "get_entity_relationships"):
                relationships = await storage.get_entity_relationships(entity_id, limit=20)
            data["relationships"] = relationships

            return JSONResponse(content={"entity": data})
        except HTTPException:
            raise
        except Exception as exc:
            logger.error("get_entity_detail failed: %s", exc, exc_info=True)
            raise HTTPException(500, detail="Internal server error")

    @router.get(
        f"{PREFIX}/entities/{{entity_id}}/claims",
        summary="Claims referencing entity",
        tags=["entities"],
    )
    async def get_entity_claims_endpoint(
        entity_id: str,
        request: Request,
        _: None = Depends(verify_api_key),
        plugin: Any = Depends(get_plugin),
    ) -> JSONResponse:
        """Return all claims where this entity is subject or object."""
        storage = getattr(plugin, "storage", None)
        if storage is None:
            raise HTTPException(503, detail="Storage not available")
        try:
            entity = await storage.get_entity(entity_id)
            if entity is None:
                raise HTTPException(404, detail=f"Entity {entity_id} not found")
            if not hasattr(storage, "get_claims_by_entity"):
                return JSONResponse(content={"claims": [], "total": 0})
            effective_owner = _resolve_owner_id(request)
            claims = await storage.get_claims_by_entity(
                entity_id,
                owner_id=effective_owner,
            )
            items = [
                _serialise(c.__dict__) if hasattr(c, "__dict__") else _serialise(c)
                for c in claims
            ]
            return JSONResponse(content={"claims": items, "total": len(items)})
        except HTTPException:
            raise
        except Exception as exc:
            logger.error("get_entity_claims failed: %s", exc, exc_info=True)
            raise HTTPException(500, detail="Internal server error")

    @router.post(
        f"{PREFIX}/entities/{{entity_id}}/merge",
        summary="Merge entity with another",
        tags=["entities"],
    )
    async def merge_entity(
        entity_id: str,
        body: MergeEntityRequest,
        _: None = Depends(verify_api_key),
        plugin: Any = Depends(get_plugin),
    ) -> JSONResponse:
        """Merge entity_id INTO target_entity_id (keep target, dissolve source)."""
        storage = getattr(plugin, "storage", None)
        if storage is None:
            raise HTTPException(503, detail="Storage not available")
        try:
            src = await storage.get_entity(entity_id)
            if src is None:
                raise HTTPException(404, detail=f"Source entity {entity_id} not found")
            tgt = await storage.get_entity(body.target_entity_id)
            if tgt is None:
                raise HTTPException(404, detail=f"Target entity {body.target_entity_id} not found")
            merged = await storage.merge_entities(
                keep_id=body.target_entity_id,
                merge_id=entity_id,
            )
            data = _serialise(merged.__dict__) if hasattr(merged, "__dict__") else _serialise(merged)
            return JSONResponse(
                content={
                    "merged_entity": data,
                    "dissolved_id": entity_id,
                    "kept_id": body.target_entity_id,
                }
            )
        except HTTPException:
            raise
        except Exception as exc:
            logger.error("merge_entity failed: %s", exc, exc_info=True)
            raise HTTPException(500, detail="Internal server error")

    # ---- GDPR forget endpoints (#108) ----

    async def gdpr_forget_entity(
        entity_id: str,
        request: Any,
        owner_id: Optional[str] = None,
        dry_run: bool = False,
    ) -> dict:
        """DELETE /api/v1/entities/{entity_id}/forget"""
        from cortex.tools.gdpr import forget_entity as _forget_entity

        if not owner_id:
            raise HTTPException(status_code=400, detail="owner_id query parameter is required")

        plugin = get_plugin()
        storage = plugin.storage
        emitter = getattr(plugin, "emitter", None)

        try:
            result = await _forget_entity(
                storage,
                entity_id=entity_id,
                owner_id=owner_id,
                dry_run=dry_run,
                emitter=emitter,
            )
        except PermissionError as exc:
            raise HTTPException(status_code=403, detail="Internal server error")
        except Exception as exc:
            raise HTTPException(status_code=500, detail=f"Forget failed: {exc}")

        return result.to_dict()

    async def gdpr_forget_owner(
        owner_id: str,
        request: Any,
        dry_run: bool = False,
    ) -> dict:
        """DELETE /api/v1/owner/{owner_id}/forget"""
        from cortex.tools.gdpr import forget_owner as _forget_owner

        confirm = request.headers.get("X-Confirm-Delete", "").lower()
        if confirm != "true":
            raise HTTPException(
                status_code=400,
                detail="Header X-Confirm-Delete: true is required for owner forget",
            )

        plugin = get_plugin()
        storage = plugin.storage
        emitter = getattr(plugin, "emitter", None)

        try:
            result = await _forget_owner(
                storage,
                owner_id=owner_id,
                dry_run=dry_run,
                emitter=emitter,
            )
        except Exception as exc:
            raise HTTPException(status_code=500, detail=f"Owner forget failed: {exc}")

        return result.to_dict()

    router.add_api_route(
        f"{PREFIX}/entities/{{entity_id}}/forget",
        methods=["DELETE"],
        endpoint=gdpr_forget_entity,
        summary="GDPR forget — hard-delete one entity and all its claims",
        tags=["gdpr"],
    )

    # ---- CANONICAL: /owners/{owner_id}/forget  (plural — issue #301) ----
    router.add_api_route(
        f"{PREFIX}/owners/{{owner_id}}/forget",
        methods=["DELETE"],
        endpoint=gdpr_forget_owner,
        summary="GDPR forget — delete ALL data for an owner (requires X-Confirm-Delete: true)",
        tags=["gdpr"],
    )

    # ---- DEPRECATED: /owner/{owner_id}/forget  (singular — issue #301) ----
    async def gdpr_forget_owner_deprecated(
        owner_id: str,
        request: Any,
        response: Response,
        dry_run: bool = False,
    ) -> dict:
        """DEPRECATED — use ``DELETE /api/v1/owners/{owner_id}/forget``.

        .. deprecated:: sunset 2026-06-01
        """
        add_deprecation_headers(response, f"{PREFIX}/owners/{owner_id}/forget")
        return await gdpr_forget_owner(owner_id=owner_id, request=request, dry_run=dry_run)

    router.add_api_route(
        f"{PREFIX}/owner/{{owner_id}}/forget",
        methods=["DELETE"],
        endpoint=gdpr_forget_owner_deprecated,
        summary="GDPR forget owner — DEPRECATED, use /owners/{owner_id}/forget",
        tags=["gdpr (deprecated)"],
        deprecated=True,
    )

    # ---- Peer promotion endpoints (#932) ----

    @router.get(
        f"{PREFIX}/peers",
        summary="List peers",
        tags=["peers"],
    )
    async def list_peers(
        request: Request,
        peer_type: Optional[str] = None,
        status: Optional[str] = None,
        _: None = Depends(verify_api_key),
        plugin: Any = Depends(get_plugin),
    ) -> JSONResponse:
        """Return all peers for the owner, with optional type/status filters."""
        storage = getattr(plugin, "storage", None)
        if storage is None:
            raise HTTPException(503, detail="Storage not available")
        if not hasattr(storage, "list_peers"):
            raise HTTPException(501, detail="Peer listing not supported by storage")
        try:
            owner_id = _resolve_owner_id(request)
            peers = await storage.list_peers(
                owner_id=owner_id,
                peer_type=peer_type,
                status=status,
            )
            items = [
                _serialise(p.__dict__) if hasattr(p, "__dict__") else _serialise(p)
                for p in peers
            ]
            return JSONResponse(content={"peers": items, "total": len(items)})
        except Exception as exc:
            logger.error("list_peers failed: %s", exc, exc_info=True)
            raise HTTPException(500, detail="Internal server error")

    @router.get(
        f"{PREFIX}/peers/{{peer_id}}",
        summary="Get peer detail",
        tags=["peers"],
    )
    async def get_peer(
        peer_id: str,
        _: None = Depends(verify_api_key),
        plugin: Any = Depends(get_plugin),
    ) -> JSONResponse:
        """Return a single peer by ID."""
        storage = getattr(plugin, "storage", None)
        if storage is None:
            raise HTTPException(503, detail="Storage not available")
        if not hasattr(storage, "get_peer"):
            raise HTTPException(501, detail="Peer lookup not supported by storage")
        try:
            peer = await storage.get_peer(peer_id)
            if peer is None:
                raise HTTPException(404, detail=f"Peer {peer_id} not found")
            data = _serialise(peer.__dict__) if hasattr(peer, "__dict__") else _serialise(peer)
            return JSONResponse(content={"peer": data})
        except HTTPException:
            raise
        except Exception as exc:
            logger.error("get_peer failed: %s", exc, exc_info=True)
            raise HTTPException(500, detail="Internal server error")
