"""
cortex/tools/gdpr.py — GDPR-compliant cascade delete for entities and owners (#108).

Provides two top-level operations:

    forget_entity(storage, entity_id, owner_id, dry_run=False) -> ForgetResult
        Hard-delete a single entity and every claim / brain-node that references
        it.  Supports dry_run mode that returns what *would* be deleted.

    forget_owner(storage, owner_id, dry_run=False) -> ForgetResult
        Nuclear option: delete ALL data for an owner — claims, entities,
        sessions, episode events, request_logs, event_log rows.

Design decisions:
  - Hard deletes only — GDPR requires actual erasure, not soft archival.
  - brain_node deletion is best-effort; the table may not exist in all installs.
  - Every non-dry-run forget_entity call writes an audit entry to claim_changelog.
  - An ENTITY_FORGOTTEN event is emitted after deletion.
  - forget_owner does NOT emit per-entity events (too noisy); it emits one
    MEMORY_DELETED event with totals in the payload.

Dependencies: cortex.storage.adapter (StorageAdapter), cortex.events.emitter
"""
from __future__ import annotations

import json
import logging
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Any, List, Optional

logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# Result type
# ---------------------------------------------------------------------------

@dataclass
class ForgetResult:
    """Summary of what was (or would be) deleted."""

    entity_id: Optional[str] = None
    owner_id: Optional[str] = None
    claims_deleted: int = 0
    entities_deleted: int = 0
    brain_nodes_deleted: int = 0
    sessions_deleted: int = 0
    events_deleted: int = 0
    request_logs_deleted: int = 0
    dry_run: bool = False
    entity_name: Optional[str] = None

    def to_dict(self) -> dict:
        return {
            "entity_id": self.entity_id,
            "owner_id": self.owner_id,
            "claims_deleted": self.claims_deleted,
            "entities_deleted": self.entities_deleted,
            "brain_nodes_deleted": self.brain_nodes_deleted,
            "sessions_deleted": self.sessions_deleted,
            "events_deleted": self.events_deleted,
            "request_logs_deleted": self.request_logs_deleted,
            "dry_run": self.dry_run,
            "entity_name": self.entity_name,
        }


# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------

def _now() -> str:
    return datetime.now(timezone.utc).isoformat(timespec="milliseconds")


async def _count_query(conn: Any, sql: str, params: tuple) -> int:
    """Execute a COUNT query and return the integer result."""
    async with conn.execute(sql, params) as cur:
        row = await cur.fetchone()
    return row[0] if row else 0


async def _brain_nodes_for_entity(conn: Any, entity_id: str) -> List[str]:
    """Return brain_graph_node IDs that reference entity_id in metadata_json.

    Gracefully returns [] if the table does not exist.
    """
    try:
        # Escape LIKE wildcards in entity_id (#220)
        safe_id = entity_id.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
        async with conn.execute(
            """
            SELECT id FROM brain_graph_node
            WHERE metadata_json LIKE ? ESCAPE '\\'
            """,
            (f'%{safe_id}%',),
        ) as cur:
            rows = await cur.fetchall()
        return [r[0] for r in rows]
    except Exception:
        # Table may not exist in this DB schema
        return []


# ---------------------------------------------------------------------------
# Public API — forget_entity
# ---------------------------------------------------------------------------

async def forget_entity(
    storage: Any,
    entity_id: str,
    owner_id: str,
    dry_run: bool = False,
    emitter: Optional[Any] = None,
) -> ForgetResult:
    """GDPR-hard-delete a single entity and all associated data.

    Parameters
    ----------
    storage:
        A StorageAdapter (SQLiteAdapter) instance.
    entity_id:
        The entity row ID to delete.
    owner_id:
        Owner scope — used as an ownership guard and for changelog authorship.
    dry_run:
        If True, return counts of what *would* be deleted without touching the DB.
    emitter:
        Optional EventEmitter.  If provided, ENTITY_FORGOTTEN is emitted after
        deletion.

    Returns
    -------
    ForgetResult with counts filled in.
    """
    conn = await storage._get_conn()

    # ---- 1. Verify entity exists and belongs to owner ----
    entity = await storage.get_entity(entity_id)
    if entity is None:
        logger.warning("forget_entity: entity %s not found — nothing to delete", entity_id)
        return ForgetResult(
            entity_id=entity_id,
            owner_id=owner_id,
            dry_run=dry_run,
        )

    entity_name = getattr(entity, "canonical_name", entity_id)
    if getattr(entity, "owner_id", None) != owner_id:
        raise PermissionError(
            f"Entity {entity_id} belongs to owner {entity.owner_id!r}, "
            f"not {owner_id!r}"
        )

    # ---- 2. Count affected rows ----
    claims_count = await _count_query(
        conn,
        """
        SELECT COUNT(*) FROM semantic_claim
        WHERE subject_entity_id = ? OR object_entity_id = ?
        """,
        (entity_id, entity_id),
    )

    brain_node_ids = await _brain_nodes_for_entity(conn, entity_id)

    result = ForgetResult(
        entity_id=entity_id,
        owner_id=owner_id,
        claims_deleted=claims_count,
        entities_deleted=1,
        brain_nodes_deleted=len(brain_node_ids),
        dry_run=dry_run,
        entity_name=entity_name,
    )

    if dry_run:
        logger.info(
            "forget_entity [DRY RUN] entity=%s name=%r — would delete %d claims, "
            "%d brain nodes",
            entity_id, entity_name, claims_count, len(brain_node_ids),
        )
        return result

    # ---- 3. Hard-delete claims ----
    # Also remove FTS index entries
    async with conn.execute(
        "SELECT id FROM semantic_claim WHERE subject_entity_id = ? OR object_entity_id = ?",
        (entity_id, entity_id),
    ) as cur:
        claim_rows = await cur.fetchall()
    claim_ids = [r[0] for r in claim_rows]

    if claim_ids:
        placeholders = ",".join("?" for _ in claim_ids)
        await conn.execute(
            f"DELETE FROM semantic_claim WHERE id IN ({placeholders})",
            claim_ids,
        )
        # Purge FTS shadow table entries
        try:
            await conn.execute(
                f"DELETE FROM fts_claims WHERE claim_id IN ({placeholders})",
                claim_ids,
            )
        except Exception:
            pass  # FTS may not exist

    # ---- 4. Hard-delete entity aliases then entity ----
    await conn.execute(
        "DELETE FROM entity_alias WHERE entity_id = ?",
        (entity_id,),
    )
    await conn.execute(
        "DELETE FROM entity WHERE id = ?",
        (entity_id,),
    )

    # ---- 5. Hard-delete referencing brain_graph_nodes ----
    for node_id in brain_node_ids:
        try:
            await conn.execute(
                "DELETE FROM brain_graph_edge WHERE source_id = ? OR target_id = ?",
                (node_id, node_id),
            )
            await conn.execute(
                "DELETE FROM brain_graph_node WHERE id = ?",
                (node_id,),
            )
        except Exception as exc:
            logger.warning("forget_entity: brain node delete failed for %s: %s", node_id, exc)

    await conn.commit()

    # ---- 6. Write audit log entry ----
    # We use event_log (no FK constraints) to record the GDPR audit trail.
    # claim_changelog requires an FK to semantic_claim.id, which is unusable
    # here because the claims have already been deleted.
    try:
        audit_payload = json.dumps({
            "action": "gdpr_forget",
            "entity_id": entity_id,
            "entity_name": entity_name,
            "claims_deleted": len(claim_ids),
            "brain_nodes_deleted": len(brain_node_ids),
            "changed_by": f"owner:{owner_id}",
        })
        await storage.log_event(
            event_type="gdpr.entity_forget",
            owner_id=owner_id,
            payload_json=audit_payload,
        )
    except Exception as exc:
        logger.warning("forget_entity: audit log write failed: %s", exc)

    # ---- 7. Emit ENTITY_FORGOTTEN event ----
    if emitter is not None:
        try:
            from cortex.events.emitter import CortexEvent, EventType
            await emitter.emit(CortexEvent(
                event_type=EventType.ENTITY_FORGOTTEN,
                owner_id=owner_id,
                payload={
                    "entity_id": entity_id,
                    "entity_name": entity_name,
                    "claims_deleted": len(claim_ids),
                    "brain_nodes_deleted": len(brain_node_ids),
                },
            ))
        except Exception as exc:
            logger.warning("forget_entity: event emit failed: %s", exc)

    logger.info(
        "forget_entity: deleted entity=%s name=%r — %d claims, %d brain nodes",
        entity_id, entity_name, len(claim_ids), len(brain_node_ids),
    )
    return result


# ---------------------------------------------------------------------------
# Public API — forget_owner
# ---------------------------------------------------------------------------

async def forget_owner(
    storage: Any,
    owner_id: str,
    dry_run: bool = False,
    emitter: Optional[Any] = None,
) -> ForgetResult:
    """Nuclear GDPR forget: hard-delete ALL data for *owner_id*.

    Tables purged (in order to respect FK constraints):
      claim_provenance, fts_claims (shadow), semantic_claim
      entity_alias, entity
      episode_event, session_record
      event_log, request_log

    brain_graph rows are owner-scoped via brain_graph_index.owner_id and
    are purged transitively (nodes → edges → index).

    Parameters
    ----------
    storage, owner_id, dry_run, emitter — same semantics as forget_entity.
    """
    conn = await storage._get_conn()

    # ---- Count everything ----
    claims_count = await _count_query(
        conn,
        "SELECT COUNT(*) FROM semantic_claim WHERE owner_id = ?",
        (owner_id,),
    )
    entities_count = await _count_query(
        conn,
        "SELECT COUNT(*) FROM entity WHERE owner_id = ?",
        (owner_id,),
    )
    sessions_count = await _count_query(
        conn,
        "SELECT COUNT(*) FROM session_record WHERE owner_id = ?",
        (owner_id,),
    )
    events_count = await _count_query(
        conn,
        "SELECT COUNT(*) FROM event_log WHERE owner_id = ?",
        (owner_id,),
    )
    request_logs_count = await _count_query(
        conn,
        "SELECT COUNT(*) FROM request_log WHERE owner_id = ?",
        (owner_id,),
    )

    # brain nodes via owner-scoped brain_graph_index
    brain_nodes_count = 0
    try:
        brain_nodes_count = await _count_query(
            conn,
            """
            SELECT COUNT(*) FROM brain_graph_node bnn
            WHERE bnn.graph_id IN (
                SELECT id FROM brain_graph_index WHERE owner_id = ?
            )
            """,
            (owner_id,),
        )
    except Exception as e:
        logger.warning("cortex_tools_gdpr: unhandled exception: %s", e)

    result = ForgetResult(
        owner_id=owner_id,
        claims_deleted=claims_count,
        entities_deleted=entities_count,
        brain_nodes_deleted=brain_nodes_count,
        sessions_deleted=sessions_count,
        events_deleted=events_count,
        request_logs_deleted=request_logs_count,
        dry_run=dry_run,
    )

    if dry_run:
        logger.info(
            "forget_owner [DRY RUN] owner=%r — would delete %d claims, "
            "%d entities, %d sessions, %d events, %d request_logs",
            owner_id, claims_count, entities_count, sessions_count,
            events_count, request_logs_count,
        )
        return result

    # ---- Hard deletes (order matters for FK constraints) ----

    # Claim provenance first (references semantic_claim)
    try:
        await conn.execute(
            """
            DELETE FROM claim_provenance
            WHERE claim_id IN (
                SELECT id FROM semantic_claim WHERE owner_id = ?
            )
            """,
            (owner_id,),
        )
    except Exception as exc:
        logger.warning("forget_owner: claim_provenance delete failed: %s", exc)

    # FTS shadow table
    try:
        await conn.execute(
            """
            DELETE FROM fts_claims
            WHERE claim_id IN (
                SELECT id FROM semantic_claim WHERE owner_id = ?
            )
            """,
            (owner_id,),
        )
    except Exception as e:
        logger.warning("cortex_tools_gdpr: unhandled exception: %s", e)

    await conn.execute(
        "DELETE FROM semantic_claim WHERE owner_id = ?", (owner_id,)
    )
    await conn.execute(
        "DELETE FROM entity_alias WHERE entity_id IN (SELECT id FROM entity WHERE owner_id = ?)",
        (owner_id,),
    )
    await conn.execute(
        "DELETE FROM entity WHERE owner_id = ?", (owner_id,)
    )

    # Episodes reference sessions; delete episodes first
    try:
        await conn.execute(
            """
            DELETE FROM episode_event
            WHERE session_id IN (
                SELECT id FROM session_record WHERE owner_id = ?
            )
            """,
            (owner_id,),
        )
    except Exception as exc:
        logger.warning("forget_owner: episode_event delete failed: %s", exc)

    await conn.execute(
        "DELETE FROM session_record WHERE owner_id = ?", (owner_id,)
    )
    await conn.execute(
        "DELETE FROM event_log WHERE owner_id = ?", (owner_id,)
    )
    await conn.execute(
        "DELETE FROM request_log WHERE owner_id = ?", (owner_id,)
    )

    # Brain graph (best-effort)
    try:
        await conn.execute(
            """
            DELETE FROM brain_graph_edge
            WHERE graph_id IN (SELECT id FROM brain_graph_index WHERE owner_id = ?)
            """,
            (owner_id,),
        )
        await conn.execute(
            """
            DELETE FROM brain_graph_node
            WHERE graph_id IN (SELECT id FROM brain_graph_index WHERE owner_id = ?)
            """,
            (owner_id,),
        )
        await conn.execute(
            "DELETE FROM brain_graph_index WHERE owner_id = ?", (owner_id,)
        )
    except Exception as exc:
        logger.warning("forget_owner: brain graph delete failed: %s", exc)

    await conn.commit()

    # Emit summary event
    if emitter is not None:
        try:
            from cortex.events.emitter import CortexEvent, EventType
            await emitter.emit(CortexEvent(
                event_type=EventType.MEMORY_DELETED,
                owner_id=owner_id,
                payload={
                    "operation": "gdpr_forget_owner",
                    "claims_deleted": claims_count,
                    "entities_deleted": entities_count,
                    "sessions_deleted": sessions_count,
                    "events_deleted": events_count,
                    "request_logs_deleted": request_logs_count,
                },
            ))
        except Exception as exc:
            logger.warning("forget_owner: event emit failed: %s", exc)

    logger.info(
        "forget_owner: owner=%r — deleted %d claims, %d entities, "
        "%d sessions, %d events, %d request_logs",
        owner_id, claims_count, entities_count, sessions_count,
        events_count, request_logs_count,
    )
    return result
