"""
Phase 47: Data Export & Portability — GDPR/Privacy Compliance.

Users and agents can export, inventory, anonymize, and delete their data.
Implements the right to data portability (Article 20) and the right to
erasure (Article 17, "right to be forgotten") from GDPR.

Features:
  - EXPORT: Collect all data about a user/agent into a portable bundle
  - FORMATS: JSON and CSV export
  - INVENTORY: List what data exists without exporting contents
  - DELETE: Full erasure with audit trail (right to be forgotten)
  - ANONYMIZE: Replace PII with placeholders, keep statistical data
  - BACKUP: Timestamped snapshots of broker data sections
  - REQUEST TRACKING: Compliance-grade export request lifecycle

Storage:
  state/broker/exports/
    requests.jsonl                          — export request tracking
    backups/{timestamp}/                    — timestamped backup snapshots
    audit.jsonl                             — deletion / anonymization audit log

Functions are sync (no async) for use from CLI and MCP tool wrappers.
"""

import csv
import io
import json
import shutil
import uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Optional

from brynq.server import (
    BROKER_DIR,
    CHANNELS_DIR,
    SESSIONS_DIR,
    _read_jsonl_sync,
    _write_json_sync,
    _append_jsonl_sync,
)

# ── Paths ──────────────────────────────────────────────────────────────────

EXPORTS_DIR = BROKER_DIR / "exports"
REQUESTS_PATH = EXPORTS_DIR / "requests.jsonl"
BACKUPS_DIR = EXPORTS_DIR / "backups"
EXPORT_AUDIT_PATH = EXPORTS_DIR / "audit.jsonl"

for _d in (EXPORTS_DIR, BACKUPS_DIR):
    _d.mkdir(parents=True, exist_ok=True)

# Section names used throughout the module
ALL_SECTIONS = (
    "profile",
    "tasks",
    "messages",
    "dms",
    "reputation",
    "activity",
    "preferences",
    "sandbox",
    "memory",
)


# ── Internal Helpers ───────────────────────────────────────────────────────


def _now_iso() -> str:
    return datetime.now(timezone.utc).isoformat()


def _log_export_audit(action: str, user_id: str, details: dict) -> None:
    """Append to the export-specific audit trail."""
    entry = {
        "id": uuid.uuid4().hex[:12],
        "timestamp": _now_iso(),
        "action": action,
        "user_id": user_id,
        "details": details,
    }
    _append_jsonl_sync(EXPORT_AUDIT_PATH, entry)


def _collect_profile(user_id: str) -> dict:
    """Load the user's profile JSON if it exists."""
    profiles_dir = BROKER_DIR / "profiles"
    path = profiles_dir / f"{user_id}.json"
    if not path.exists():
        return {}
    try:
        return json.loads(path.read_text("utf-8"))
    except (json.JSONDecodeError, OSError):
        return {}


def _collect_tasks(user_id: str) -> list[dict]:
    """Return all tasks created by or assigned to the user."""
    tasks_dir = BROKER_DIR / "tasks"
    if not tasks_dir.exists():
        return []
    results = []
    for f in tasks_dir.glob("*.json"):
        try:
            task = json.loads(f.read_text("utf-8"))
            if task.get("created_by") == user_id or task.get("assigned_to") == user_id:
                results.append(task)
        except (json.JSONDecodeError, OSError):
            continue
    return results


def _collect_messages(user_id: str) -> list[dict]:
    """Return all channel messages sent by the user."""
    if not CHANNELS_DIR.exists():
        return []
    results = []
    for f in CHANNELS_DIR.glob("*.jsonl"):
        for entry in _read_jsonl_sync(f):
            if entry.get("session_id") == user_id:
                results.append(entry)
    return results


def _collect_dms(user_id: str) -> list[dict]:
    """Return all DMs sent by the user."""
    dms_dir = BROKER_DIR / "dms"
    if not dms_dir.exists():
        return []
    results = []
    for f in dms_dir.glob("*.jsonl"):
        for entry in _read_jsonl_sync(f):
            if entry.get("sender_id") == user_id:
                results.append(entry)
    return results


def _collect_reputation(user_id: str) -> list[dict]:
    """Return all rating entries involving the user."""
    ratings_path = BROKER_DIR / "profiles" / "_ratings.jsonl"
    if not ratings_path.exists():
        return []
    results = []
    for entry in _read_jsonl_sync(ratings_path):
        if entry.get("rated_id") == user_id or entry.get("rater_id") == user_id:
            results.append(entry)
    return results


def _collect_activity(user_id: str) -> list[dict]:
    """Return audit trail entries for the user."""
    audit_path = BROKER_DIR / "audit" / "trail.jsonl"
    if not audit_path.exists():
        return []
    results = []
    for entry in _read_jsonl_sync(audit_path):
        if entry.get("actor_id") == user_id:
            results.append(entry)
    return results


def _collect_preferences(user_id: str) -> dict:
    """Return the user's notification preferences."""
    prefs_path = BROKER_DIR / "notification_prefs" / f"{user_id}.json"
    if not prefs_path.exists():
        return {}
    try:
        return json.loads(prefs_path.read_text("utf-8"))
    except (json.JSONDecodeError, OSError):
        return {}


def _collect_sandbox(user_id: str) -> dict:
    """Return the user's sandbox configuration."""
    sandbox_path = BROKER_DIR / "sandboxes" / f"{user_id}.json"
    if not sandbox_path.exists():
        return {}
    try:
        return json.loads(sandbox_path.read_text("utf-8"))
    except (json.JSONDecodeError, OSError):
        return {}


def _collect_memory(user_id: str) -> list[dict]:
    """Return all memory entries for the user."""
    mem_dir = BROKER_DIR / "memory" / user_id
    if not mem_dir.exists():
        return []
    results = []
    for f in mem_dir.glob("*.json"):
        if f.name.startswith("_"):
            continue
        try:
            results.append(json.loads(f.read_text("utf-8")))
        except (json.JSONDecodeError, OSError):
            continue
    return results


# Section collector registry — maps section name to a callable
_COLLECTORS: dict[str, Any] = {
    "profile": _collect_profile,
    "tasks": _collect_tasks,
    "messages": _collect_messages,
    "dms": _collect_dms,
    "reputation": _collect_reputation,
    "activity": _collect_activity,
    "preferences": _collect_preferences,
    "sandbox": _collect_sandbox,
    "memory": _collect_memory,
}


# ── 1. Export User Data ───────────────────────────────────────────────────


def export_user_data(user_id: str) -> dict:
    """Collect ALL data about a user/agent across all platform sections.

    Returns a dict keyed by section name with the collected data.
    """
    if not user_id:
        raise ValueError("user_id is required")

    data: dict[str, Any] = {
        "export_metadata": {
            "user_id": user_id,
            "exported_at": _now_iso(),
            "sections": list(ALL_SECTIONS),
        },
    }

    for section, collector in _COLLECTORS.items():
        data[section] = collector(user_id)

    _log_export_audit("export", user_id, {
        "sections": list(ALL_SECTIONS),
        "section_counts": {s: _count(data[s]) for s in ALL_SECTIONS},
    })

    return data


def _count(value: Any) -> int:
    """Count items in a section value — len for list/dict, 1 if truthy, 0 if empty."""
    if isinstance(value, list):
        return len(value)
    if isinstance(value, dict):
        return len(value)
    return 1 if value else 0


# ── 2. Export Formats ─────────────────────────────────────────────────────


def export_as_json(user_id: str) -> str:
    """Export all user data as a formatted JSON string."""
    data = export_user_data(user_id)
    return json.dumps(data, indent=2, default=str)


def export_as_csv(user_id: str) -> dict[str, str]:
    """Export user data as a dict of section_name -> CSV string.

    Each section that contains list data becomes a CSV table.
    Dict sections (profile, preferences, sandbox) are converted to
    key-value rows.
    """
    data = export_user_data(user_id)
    csvs: dict[str, str] = {}

    for section in ALL_SECTIONS:
        section_data = data.get(section)
        if not section_data:
            continue
        csvs[section] = _to_csv(section, section_data)

    return csvs


def _to_csv(section: str, data: Any) -> str:
    """Convert section data to a CSV string."""
    output = io.StringIO()

    if isinstance(data, list) and data:
        # List of dicts -> table with header row from first dict's keys
        if isinstance(data[0], dict):
            all_keys: list[str] = []
            seen: set[str] = set()
            for row in data:
                for k in row.keys():
                    if k not in seen:
                        all_keys.append(k)
                        seen.add(k)
            writer = csv.DictWriter(output, fieldnames=all_keys, extrasaction="ignore")
            writer.writeheader()
            for row in data:
                writer.writerow({k: json.dumps(v, default=str) if isinstance(v, (dict, list)) else v for k, v in row.items()})
        else:
            writer_simple = csv.writer(output)
            writer_simple.writerow(["value"])
            for item in data:
                writer_simple.writerow([item])
    elif isinstance(data, dict):
        # Dict -> key/value rows
        writer_kv = csv.writer(output)
        writer_kv.writerow(["key", "value"])
        for k, v in data.items():
            display = json.dumps(v, default=str) if isinstance(v, (dict, list)) else v
            writer_kv.writerow([k, display])
    else:
        output.write(str(data))

    return output.getvalue()


# ── 3. Data Inventory ────────────────────────────────────────────────────


def get_data_inventory(user_id: str) -> dict[str, int]:
    """List what data exists for a user without exporting contents.

    Returns {section: count} for each data type.
    """
    if not user_id:
        raise ValueError("user_id is required")

    inventory: dict[str, int] = {}
    for section, collector in _COLLECTORS.items():
        result = collector(user_id)
        inventory[section] = _count(result)

    return inventory


# ── 4. Delete User Data ──────────────────────────────────────────────────


def delete_user_data(user_id: str, confirm: bool = False) -> list[str]:
    """Remove all user data from the platform (right to be forgotten).

    Requires ``confirm=True`` to actually perform deletion.
    Logs deletion to the export audit trail.

    Returns a list of descriptions of what was deleted.
    """
    if not user_id:
        raise ValueError("user_id is required")
    if not confirm:
        raise ValueError("confirm=True is required to delete user data")

    deleted: list[str] = []

    # Profile
    profile_path = BROKER_DIR / "profiles" / f"{user_id}.json"
    if profile_path.exists():
        profile_path.unlink()
        deleted.append("profile")

    # Tasks created by or assigned to user
    tasks_dir = BROKER_DIR / "tasks"
    if tasks_dir.exists():
        for f in tasks_dir.glob("*.json"):
            try:
                task = json.loads(f.read_text("utf-8"))
                if task.get("created_by") == user_id or task.get("assigned_to") == user_id:
                    f.unlink()
                    deleted.append(f"task:{task.get('task_id', f.stem)}")
            except (json.JSONDecodeError, OSError):
                continue

    # Channel messages — rewrite JSONL files without user's messages
    if CHANNELS_DIR.exists():
        for f in CHANNELS_DIR.glob("*.jsonl"):
            entries = _read_jsonl_sync(f)
            original_count = len(entries)
            filtered = [e for e in entries if e.get("session_id") != user_id]
            if len(filtered) < original_count:
                _rewrite_jsonl(f, filtered)
                removed = original_count - len(filtered)
                deleted.append(f"messages:{f.stem}:{removed}")

    # DMs — rewrite JSONL files without user's messages
    dms_dir = BROKER_DIR / "dms"
    if dms_dir.exists():
        for f in dms_dir.glob("*.jsonl"):
            entries = _read_jsonl_sync(f)
            original_count = len(entries)
            filtered = [e for e in entries if e.get("sender_id") != user_id]
            if len(filtered) < original_count:
                _rewrite_jsonl(f, filtered)
                removed = original_count - len(filtered)
                deleted.append(f"dms:{f.stem}:{removed}")

    # Ratings
    ratings_path = BROKER_DIR / "profiles" / "_ratings.jsonl"
    if ratings_path.exists():
        entries = _read_jsonl_sync(ratings_path)
        original_count = len(entries)
        filtered = [e for e in entries if e.get("rated_id") != user_id and e.get("rater_id") != user_id]
        if len(filtered) < original_count:
            _rewrite_jsonl(ratings_path, filtered)
            deleted.append(f"ratings:{original_count - len(filtered)}")

    # Notification preferences
    prefs_path = BROKER_DIR / "notification_prefs" / f"{user_id}.json"
    if prefs_path.exists():
        prefs_path.unlink()
        deleted.append("preferences")

    # Sandbox config
    sandbox_path = BROKER_DIR / "sandboxes" / f"{user_id}.json"
    if sandbox_path.exists():
        sandbox_path.unlink()
        deleted.append("sandbox")

    # Memory
    mem_dir = BROKER_DIR / "memory" / user_id
    if mem_dir.exists():
        shutil.rmtree(mem_dir)
        deleted.append("memory")

    # Session files
    session_path = SESSIONS_DIR / f"{user_id}.json"
    if session_path.exists():
        session_path.unlink()
        deleted.append("session")

    # DM cursors for this user
    dm_cursors_dir = BROKER_DIR / "dms" / "cursors"
    if dm_cursors_dir.exists():
        for f in dm_cursors_dir.glob(f"{user_id}_*"):
            f.unlink()
            deleted.append(f"dm_cursor:{f.name}")

    # Broker cursors for this user
    cursors_dir = BROKER_DIR / "cursors"
    if cursors_dir.exists():
        for f in cursors_dir.glob(f"{user_id}_*"):
            f.unlink()
            deleted.append(f"cursor:{f.name}")

    # DM blocks for this user
    dm_blocks_dir = BROKER_DIR / "dms" / "blocks"
    if dm_blocks_dir.exists():
        blocks_path = dm_blocks_dir / f"{user_id}.json"
        if blocks_path.exists():
            blocks_path.unlink()
            deleted.append("dm_blocks")

    # Audit trail: log the deletion (we do NOT delete audit entries — that would
    # compromise the audit trail's integrity, which is a legitimate interest override)
    _log_export_audit("delete", user_id, {
        "deleted_sections": deleted,
        "count": len(deleted),
    })

    return deleted


def _rewrite_jsonl(path: Path, entries: list[dict]) -> None:
    """Rewrite a JSONL file with the given entries (atomic via temp + rename)."""
    import os
    tmp = path.with_suffix(".tmp")
    with tmp.open("w", encoding="utf-8") as f:
        for entry in entries:
            f.write(json.dumps(entry, default=str) + "\n")
    os.replace(str(tmp), str(path))


# ── 5. Anonymize ─────────────────────────────────────────────────────────


def anonymize_user_data(user_id: str) -> dict:
    """Replace identifying info with anonymous placeholders.

    Keeps statistical data (task counts, ratings, reputation scores)
    intact for analytics. Replaces: session_name, bio, email-like fields,
    personality_id.

    Returns a summary of what was anonymized.
    """
    if not user_id:
        raise ValueError("user_id is required")

    anon_id = f"anon-{uuid.uuid4().hex[:8]}"
    anonymized: list[str] = []

    # Profile
    profile_path = BROKER_DIR / "profiles" / f"{user_id}.json"
    if profile_path.exists():
        try:
            profile = json.loads(profile_path.read_text("utf-8"))
            profile["session_name"] = anon_id
            profile["bio"] = "Anonymized user"
            profile["personality_id"] = None
            if "email" in profile:
                profile["email"] = f"{anon_id}@anon.local"
            profile["anonymized"] = True
            profile["anonymized_at"] = _now_iso()
            _write_json_sync(profile_path, profile)
            anonymized.append("profile")
        except (json.JSONDecodeError, OSError):
            pass

    # Channel messages — replace session_name
    if CHANNELS_DIR.exists():
        for f in CHANNELS_DIR.glob("*.jsonl"):
            entries = _read_jsonl_sync(f)
            changed = False
            for entry in entries:
                if entry.get("session_id") == user_id:
                    entry["session_name"] = anon_id
                    changed = True
            if changed:
                _rewrite_jsonl(f, entries)
                anonymized.append(f"messages:{f.stem}")

    # DMs — replace sender_name
    dms_dir = BROKER_DIR / "dms"
    if dms_dir.exists():
        for f in dms_dir.glob("*.jsonl"):
            entries = _read_jsonl_sync(f)
            changed = False
            for entry in entries:
                if entry.get("sender_id") == user_id:
                    entry["sender_name"] = anon_id
                    changed = True
            if changed:
                _rewrite_jsonl(f, entries)
                anonymized.append(f"dms:{f.stem}")

    _log_export_audit("anonymize", user_id, {
        "anon_id": anon_id,
        "anonymized_sections": anonymized,
    })

    return {
        "user_id": user_id,
        "anon_id": anon_id,
        "anonymized_at": _now_iso(),
        "sections_anonymized": anonymized,
    }


# ── 6. Backup & Restore ──────────────────────────────────────────────────

# Sections that can be backed up (map name -> relative dir under BROKER_DIR)
BACKUP_SECTIONS: dict[str, str] = {
    "profiles": "profiles",
    "tasks": "tasks",
    "channels": "channels",
    "dms": "dms",
    "memory": "memory",
    "audit": "audit",
    "preferences": "notification_prefs",
    "sandboxes": "sandboxes",
    "sessions": "sessions",
}


def create_backup(sections: Optional[list[str]] = None) -> dict:
    """Create a timestamped backup of specified sections (or all).

    Copies the relevant directories into
    ``state/broker/exports/backups/{timestamp}/``.

    Returns metadata about the backup.
    """
    ts = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
    backup_id = f"{ts}-{uuid.uuid4().hex[:6]}"
    backup_dir = BACKUPS_DIR / backup_id
    backup_dir.mkdir(parents=True, exist_ok=True)

    target_sections = sections or list(BACKUP_SECTIONS.keys())
    backed_up: list[str] = []
    file_count = 0

    for section in target_sections:
        rel = BACKUP_SECTIONS.get(section)
        if rel is None:
            continue
        src = BROKER_DIR / rel
        if not src.exists():
            continue
        dst = backup_dir / rel
        shutil.copytree(src, dst, dirs_exist_ok=True)
        backed_up.append(section)
        file_count += sum(1 for _ in dst.rglob("*") if _.is_file())

    meta = {
        "backup_id": backup_id,
        "created_at": _now_iso(),
        "sections": backed_up,
        "file_count": file_count,
    }
    _write_json_sync(backup_dir / "_meta.json", meta)
    return meta


def list_backups() -> list[dict]:
    """Return metadata for all existing backups, newest first."""
    results: list[dict] = []
    if not BACKUPS_DIR.exists():
        return results
    for d in sorted(BACKUPS_DIR.iterdir(), reverse=True):
        if not d.is_dir():
            continue
        meta_path = d / "_meta.json"
        if meta_path.exists():
            try:
                results.append(json.loads(meta_path.read_text("utf-8")))
            except (json.JSONDecodeError, OSError):
                results.append({"backup_id": d.name, "error": "corrupt metadata"})
        else:
            results.append({"backup_id": d.name, "sections": [], "file_count": 0})
    return results


def restore_backup(backup_id: str, sections: Optional[list[str]] = None) -> dict:
    """Restore data from a backup.

    If ``sections`` is provided, only those sections are restored.
    Otherwise all backed-up sections are restored.

    Returns a summary of what was restored.
    """
    backup_dir = BACKUPS_DIR / backup_id
    if not backup_dir.exists():
        raise FileNotFoundError(f"Backup not found: {backup_id}")

    meta_path = backup_dir / "_meta.json"
    available_sections: list[str] = []
    if meta_path.exists():
        try:
            meta = json.loads(meta_path.read_text("utf-8"))
            available_sections = meta.get("sections", [])
        except (json.JSONDecodeError, OSError):
            pass

    target_sections = sections or available_sections or list(BACKUP_SECTIONS.keys())
    restored: list[str] = []
    file_count = 0

    for section in target_sections:
        rel = BACKUP_SECTIONS.get(section)
        if rel is None:
            continue
        src = backup_dir / rel
        if not src.exists():
            continue
        dst = BROKER_DIR / rel
        dst.mkdir(parents=True, exist_ok=True)
        shutil.copytree(src, dst, dirs_exist_ok=True)
        restored.append(section)
        file_count += sum(1 for _ in src.rglob("*") if _.is_file())

    result = {
        "backup_id": backup_id,
        "restored_at": _now_iso(),
        "sections_restored": restored,
        "file_count": file_count,
    }

    _log_export_audit("restore", "system", {
        "backup_id": backup_id,
        "sections_restored": restored,
    })

    return result


# ── 7. Export Request Tracking ───────────────────────────────────────────

VALID_REQUEST_STATUSES = ("pending", "in_progress", "completed", "failed")


def request_export(user_id: str) -> dict:
    """Create a tracked export request for compliance purposes.

    Returns the request record including its ID and status.
    """
    if not user_id:
        raise ValueError("user_id is required")

    request_id = uuid.uuid4().hex[:12]
    now = _now_iso()

    record = {
        "request_id": request_id,
        "user_id": user_id,
        "status": "pending",
        "requested_at": now,
        "completed_at": None,
        "notes": None,
    }

    _append_jsonl_sync(REQUESTS_PATH, record)

    _log_export_audit("export_requested", user_id, {"request_id": request_id})

    return record


def get_export_requests(status: Optional[str] = None) -> list[dict]:
    """List export requests, optionally filtered by status.

    Returns all requests if ``status`` is None.
    """
    entries = _read_jsonl_sync(REQUESTS_PATH)
    if status is not None:
        entries = [e for e in entries if e.get("status") == status]
    return entries


def complete_export(request_id: str, notes: Optional[str] = None) -> dict:
    """Mark an export request as completed.

    Rewrites the requests file with the updated status.
    Returns the updated request record.

    Raises ValueError if the request ID is not found.
    """
    entries = _read_jsonl_sync(REQUESTS_PATH)
    found = False
    updated_record: dict = {}

    for entry in entries:
        if entry.get("request_id") == request_id:
            entry["status"] = "completed"
            entry["completed_at"] = _now_iso()
            if notes:
                entry["notes"] = notes
            updated_record = entry
            found = True
            break

    if not found:
        raise ValueError(f"Export request not found: {request_id}")

    _rewrite_jsonl(REQUESTS_PATH, entries)

    _log_export_audit("export_completed", updated_record.get("user_id", "unknown"), {
        "request_id": request_id,
    })

    return updated_record
