# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# ORCID: 0009-0006-4957-0540
# Email: Georgescottfoley@proton.me
# Licensed under the MIT License - see LICENSE file for details

"""
Consent & Data Governance Manager

Manages user consent for training data usage with full chain-of-custody
tracking. Supports:
  - Granular consent scopes (local, co-op, public)
  - Data classification (private, shareable, public)
  - Right to erasure (GDPR-style) with chain anchoring
  - Consent verification for pipeline gating

Storage: SQLite at ~/.familiar/data/consent.db (tenant-scoped)

Dependencies: None (pure stdlib)
"""

import json
import logging
import sqlite3
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from pathlib import Path
from typing import Optional

logger = logging.getLogger(__name__)


class ConsentScope(str, Enum):
    """Scopes a user can consent to for training data usage."""

    LOCAL_TRAINING = "local_training"
    COOP_SHARING = "coop_sharing"
    PUBLIC_DATASET = "public_dataset"
    NONE = "none"


class DataClassification(str, Enum):
    """Classification level for training records."""

    PRIVATE = "private"
    SHAREABLE = "shareable"
    PUBLIC = "public"


@dataclass
class ConsentRecord:
    """A consent grant or revocation for a specific user."""

    user_id: str
    scopes: list = field(default_factory=list)
    granted_at: str = ""
    revoked_at: str = ""
    chain_block_hash: str = ""
    metadata: dict = field(default_factory=dict)


class ConsentManager:
    """
    Manages consent records with SQLite persistence and chain anchoring.

    Each consent grant/revocation is recorded in the database and optionally
    anchored to the genesis block chain for immutable audit trail.
    """

    def __init__(self, data_dir: Optional[Path] = None):
        if data_dir is None:
            try:
                from familiar.core.paths import DATA_DIR

                data_dir = DATA_DIR
            except ImportError:
                data_dir = Path.home() / ".familiar" / "data"

        data_dir.mkdir(parents=True, exist_ok=True)
        self._db_path = data_dir / "consent.db"
        self._init_db()

    def _init_db(self):
        """Initialize the consent database schema."""
        with sqlite3.connect(str(self._db_path)) as conn:
            conn.execute("""
                CREATE TABLE IF NOT EXISTS consent_records (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    user_id TEXT NOT NULL,
                    scopes TEXT NOT NULL,
                    granted_at TEXT NOT NULL,
                    revoked_at TEXT DEFAULT '',
                    chain_block_hash TEXT DEFAULT '',
                    metadata TEXT DEFAULT '{}',
                    UNIQUE(user_id)
                )
            """)
            conn.execute("""
                CREATE TABLE IF NOT EXISTS consent_audit (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    user_id TEXT NOT NULL,
                    action TEXT NOT NULL,
                    scopes TEXT NOT NULL,
                    timestamp TEXT NOT NULL,
                    chain_block_hash TEXT DEFAULT '',
                    details TEXT DEFAULT '{}'
                )
            """)
            conn.execute("""
                CREATE TABLE IF NOT EXISTS erasure_records (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    user_id TEXT NOT NULL,
                    erased_at TEXT NOT NULL,
                    records_deleted INTEGER DEFAULT 0,
                    chain_block_hash TEXT DEFAULT '',
                    details TEXT DEFAULT '{}'
                )
            """)

    def grant_consent(self, user_id: str, scopes: list) -> ConsentRecord:
        """
        Grant consent for the given scopes. Updates existing record or creates new.

        Args:
            user_id: User identifier (genesis_hash or user_id)
            scopes: List of ConsentScope values

        Returns:
            The created/updated ConsentRecord
        """
        now = datetime.now().isoformat()
        scope_strs = [s.value if isinstance(s, ConsentScope) else str(s) for s in scopes]

        record = ConsentRecord(
            user_id=user_id,
            scopes=scope_strs,
            granted_at=now,
        )

        with sqlite3.connect(str(self._db_path)) as conn:
            # Upsert: replace existing record
            conn.execute(
                """INSERT OR REPLACE INTO consent_records
                   (user_id, scopes, granted_at, revoked_at, chain_block_hash, metadata)
                   VALUES (?, ?, ?, '', '', '{}')""",
                (user_id, json.dumps(scope_strs), now),
            )
            # Audit log
            conn.execute(
                """INSERT INTO consent_audit
                   (user_id, action, scopes, timestamp)
                   VALUES (?, 'grant', ?, ?)""",
                (user_id, json.dumps(scope_strs), now),
            )

        # Anchor to chain
        block_hash = self._anchor_consent(
            {
                "action": "grant",
                "user_id": user_id,
                "scopes": scope_strs,
                "timestamp": now,
            }
        )
        if block_hash:
            record.chain_block_hash = block_hash
            with sqlite3.connect(str(self._db_path)) as conn:
                conn.execute(
                    "UPDATE consent_records SET chain_block_hash = ? WHERE user_id = ?",
                    (block_hash, user_id),
                )

        logger.info("Consent granted for %s: %s", user_id, scope_strs)
        return record

    def revoke_consent(self, user_id: str, scopes: list) -> ConsentRecord:
        """
        Revoke consent for the given scopes.

        If all scopes are revoked, sets revoked_at timestamp.
        """
        now = datetime.now().isoformat()
        scope_strs = [s.value if isinstance(s, ConsentScope) else str(s) for s in scopes]

        existing = self.get_consent(user_id)
        if existing is None:
            record = ConsentRecord(user_id=user_id, scopes=[], revoked_at=now)
            return record

        # Remove revoked scopes
        remaining = [s for s in existing.scopes if s not in scope_strs]

        with sqlite3.connect(str(self._db_path)) as conn:
            if remaining:
                conn.execute(
                    "UPDATE consent_records SET scopes = ? WHERE user_id = ?",
                    (json.dumps(remaining), user_id),
                )
            else:
                conn.execute(
                    "UPDATE consent_records SET scopes = '[]', revoked_at = ? WHERE user_id = ?",
                    (now, user_id),
                )
            # Audit log
            conn.execute(
                """INSERT INTO consent_audit
                   (user_id, action, scopes, timestamp)
                   VALUES (?, 'revoke', ?, ?)""",
                (user_id, json.dumps(scope_strs), now),
            )

        # Anchor revocation
        self._anchor_consent(
            {
                "action": "revoke",
                "user_id": user_id,
                "scopes_revoked": scope_strs,
                "scopes_remaining": remaining,
                "timestamp": now,
            }
        )

        record = ConsentRecord(
            user_id=user_id,
            scopes=remaining,
            granted_at=existing.granted_at,
            revoked_at=now if not remaining else "",
        )
        logger.info("Consent revoked for %s: %s", user_id, scope_strs)
        return record

    def get_consent(self, user_id: str) -> Optional[ConsentRecord]:
        """Get the current consent record for a user."""
        with sqlite3.connect(str(self._db_path)) as conn:
            row = conn.execute(
                "SELECT user_id, scopes, granted_at, revoked_at, chain_block_hash, metadata "
                "FROM consent_records WHERE user_id = ?",
                (user_id,),
            ).fetchone()

        if row is None:
            return None

        return ConsentRecord(
            user_id=row[0],
            scopes=json.loads(row[1]) if row[1] else [],
            granted_at=row[2] or "",
            revoked_at=row[3] or "",
            chain_block_hash=row[4] or "",
            metadata=json.loads(row[5]) if row[5] else {},
        )

    def check_consent(self, user_id: str, scope: str) -> bool:
        """Check if a user has consented to a specific scope."""
        if isinstance(scope, ConsentScope):
            scope = scope.value

        record = self.get_consent(user_id)
        if record is None:
            return False
        if record.revoked_at:
            return False
        return scope in record.scopes

    def erase_user_data(self, user_id: str) -> dict:
        """
        Right to erasure: delete all training data for a user.

        Removes curated records, revokes consent, and anchors the erasure
        to the genesis chain for auditability.

        Returns dict with erasure details.
        """
        now = datetime.now().isoformat()
        records_deleted = 0

        # Delete from curated JSONL files
        try:
            from familiar.skills.training.skill import _get_curated_dir, _read_jsonl

            curated_dir = _get_curated_dir()
            for src_name in ("conversations", "tool_calls", "generated", "memories"):
                filepath = curated_dir / f"{src_name}.jsonl"
                if not filepath.exists():
                    continue

                records = _read_jsonl(filepath)
                filtered = []
                for r in records:
                    author = r.get("provenance", {}).get("author", {})
                    author_id = author.get("id", "") if isinstance(author, dict) else str(author)
                    if author_id == user_id:
                        records_deleted += 1
                    else:
                        filtered.append(r)

                # Rewrite file without erased records
                if records_deleted > 0:
                    import json as _json

                    with open(filepath, "w", encoding="utf-8") as f:
                        for rec in filtered:
                            f.write(_json.dumps(rec, default=str) + "\n")
        except ImportError:
            logger.debug("Training skill not available for erasure")

        # Revoke all consent
        self.revoke_consent(user_id, [s.value for s in ConsentScope if s != ConsentScope.NONE])

        # Record erasure
        details = {
            "user_id": user_id,
            "erased_at": now,
            "records_deleted": records_deleted,
        }

        with sqlite3.connect(str(self._db_path)) as conn:
            conn.execute(
                """INSERT INTO erasure_records
                   (user_id, erased_at, records_deleted, details)
                   VALUES (?, ?, ?, ?)""",
                (user_id, now, records_deleted, json.dumps(details)),
            )

        # Anchor erasure to chain
        block_hash = self._anchor_erasure(details)
        if block_hash:
            details["chain_block_hash"] = block_hash
            with sqlite3.connect(str(self._db_path)) as conn:
                conn.execute(
                    "UPDATE erasure_records SET chain_block_hash = ? WHERE user_id = ? AND erased_at = ?",
                    (block_hash, user_id, now),
                )

        logger.info("Erased %d records for user %s", records_deleted, user_id)
        return details

    def classify_record(self, record: dict, user_id: str) -> DataClassification:
        """
        Classify a training record based on user's consent scope.

        Returns the highest classification the record is allowed.
        """
        consent = self.get_consent(user_id)
        if consent is None or consent.revoked_at:
            return DataClassification.PRIVATE

        if ConsentScope.PUBLIC_DATASET.value in consent.scopes:
            return DataClassification.PUBLIC
        if ConsentScope.COOP_SHARING.value in consent.scopes:
            return DataClassification.SHAREABLE
        if ConsentScope.LOCAL_TRAINING.value in consent.scopes:
            return DataClassification.PRIVATE

        return DataClassification.PRIVATE

    def get_audit_log(self, user_id: str = None, limit: int = 50) -> list:
        """Get consent audit trail, optionally filtered by user."""
        with sqlite3.connect(str(self._db_path)) as conn:
            if user_id:
                rows = conn.execute(
                    "SELECT user_id, action, scopes, timestamp, chain_block_hash "
                    "FROM consent_audit WHERE user_id = ? ORDER BY id DESC LIMIT ?",
                    (user_id, limit),
                ).fetchall()
            else:
                rows = conn.execute(
                    "SELECT user_id, action, scopes, timestamp, chain_block_hash "
                    "FROM consent_audit ORDER BY id DESC LIMIT ?",
                    (limit,),
                ).fetchall()

        return [
            {
                "user_id": r[0],
                "action": r[1],
                "scopes": json.loads(r[2]) if r[2] else [],
                "timestamp": r[3],
                "chain_block_hash": r[4] or "",
            }
            for r in rows
        ]

    def _anchor_consent(self, entry: dict) -> str:
        """Anchor consent event to genesis chain. Returns block hash or empty string."""
        try:
            from familiar.skills.training.bridge import anchor_consent

            return anchor_consent(entry)
        except Exception:
            logger.debug("Chain anchor skipped for consent", exc_info=True)
            return ""

    def _anchor_erasure(self, entry: dict) -> str:
        """Anchor erasure event to genesis chain. Returns block hash or empty string."""
        try:
            from familiar.skills.training.bridge import anchor_erasure

            return anchor_erasure(entry)
        except Exception:
            logger.debug("Chain anchor skipped for erasure", exc_info=True)
            return ""
