# 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

"""
Data Isolation Layer - Phase 1

Provides isolated data storage for multi-user deployments.
Each user gets their own encrypted data directory.
Shared org data lives in a common area with permission checks.

Directory Structure:
    ~/.familiar/
    ├── config.yaml          # Global config
    ├── users.db             # User database (encrypted)
    └── data/
        ├── shared/          # Org-wide data
        │   ├── donors/      # Donor records (admin/staff)
        │   ├── knowledge/   # Shared knowledge base
        │   └── templates/   # Shared templates
        └── users/
            ├── user_abc123/ # Per-user isolated
            │   ├── memory.json
            │   ├── history/
            │   ├── tasks.json
            │   └── settings.json
            └── user_def456/
                └── ...
"""

import json
import logging
import shutil
from dataclasses import dataclass
from datetime import datetime, timezone
from enum import Enum
from pathlib import Path
from typing import Any, List, Optional

logger = logging.getLogger(__name__)

# Use centralized paths
try:
    from .paths import DATA_DIR, ensure_dir
except ImportError:
    DATA_DIR = Path.home() / ".familiar" / "data"

    def ensure_dir(p):
        Path(p).mkdir(parents=True, exist_ok=True)
        return p


# Directory structure
SHARED_DIR = DATA_DIR / "shared"
USERS_DIR = DATA_DIR / "users"


class DataScope(str, Enum):
    """Data access scope."""

    USER = "user"  # User's private data
    SHARED = "shared"  # Org-wide shared data
    SYSTEM = "system"  # System data (admin only)


class DataCategory(str, Enum):
    """Categories of data storage."""

    MEMORY = "memory"  # Conversation memory
    HISTORY = "history"  # Chat history
    TASKS = "tasks"  # Tasks and todos
    SETTINGS = "settings"  # User preferences
    DONORS = "donors"  # Donor records (shared)
    KNOWLEDGE = "knowledge"  # Knowledge base docs
    TEMPLATES = "templates"  # Message templates
    AUDIT = "audit"  # Audit logs (system)


# Permission matrix: which roles can access what
PERMISSIONS = {
    # (scope, category): [roles that can read], [roles that can write]
    (DataScope.USER, DataCategory.MEMORY): (["admin", "staff", "readonly"], ["admin", "staff"]),
    (DataScope.USER, DataCategory.HISTORY): (["admin", "staff", "readonly"], ["admin", "staff"]),
    (DataScope.USER, DataCategory.TASKS): (["admin", "staff", "readonly"], ["admin", "staff"]),
    (DataScope.USER, DataCategory.SETTINGS): (["admin", "staff", "readonly"], ["admin", "staff"]),
    (DataScope.SHARED, DataCategory.DONORS): (["admin", "staff"], ["admin", "staff"]),
    (DataScope.SHARED, DataCategory.KNOWLEDGE): (
        ["admin", "staff", "readonly"],
        ["admin", "staff"],
    ),
    (DataScope.SHARED, DataCategory.TEMPLATES): (["admin", "staff", "readonly"], ["admin"]),
    (DataScope.SYSTEM, DataCategory.AUDIT): (["admin"], ["admin"]),
}


@dataclass
class DataPath:
    """Resolved data path with metadata."""

    path: Path
    scope: DataScope
    category: DataCategory
    user_id: Optional[str]

    @property
    def exists(self) -> bool:
        return self.path.exists()

    def ensure(self) -> "DataPath":
        """Ensure the path's parent directory exists."""
        ensure_dir(self.path.parent)
        return self


class DataStore:
    """
    Isolated data storage with permission checks.

    Usage:
        store = DataStore(user_context)

        # User data (isolated)
        tasks = store.read_json(DataCategory.TASKS)
        store.write_json(DataCategory.TASKS, tasks)

        # Shared data (org-wide)
        donors = store.read_json(DataCategory.DONORS, scope=DataScope.SHARED)
    """

    def __init__(self, user_id: str, user_role: str = "staff"):
        """
        Initialize data store for a user.

        Args:
            user_id: The user's ID
            user_role: The user's role (admin, staff, readonly)
        """
        import re

        self.user_id = user_id
        self.user_role = user_role
        # Sanitize user_id for filesystem safety (prevent path traversal)
        safe_user_id = re.sub(r"[^a-zA-Z0-9_\-.]", "_", str(user_id))
        self.user_dir = USERS_DIR / safe_user_id

        # Ensure directories exist
        ensure_dir(self.user_dir)
        ensure_dir(SHARED_DIR)

    def _check_permission(
        self, scope: DataScope, category: DataCategory, write: bool = False
    ) -> bool:
        """Check if user has permission for the operation."""
        key = (scope, category)
        if key not in PERMISSIONS:
            logger.warning(f"Unknown permission key: {key}")
            return self.user_role == "admin"  # Admin can do anything

        read_roles, write_roles = PERMISSIONS[key]

        if write:
            return self.user_role in write_roles
        else:
            return self.user_role in read_roles

    def _get_path(
        self,
        category: DataCategory,
        scope: DataScope = DataScope.USER,
        filename: str = None,
        target_user_id: str = None,
    ) -> DataPath:
        """
        Get the file path for a data category.

        Args:
            category: The data category
            scope: USER, SHARED, or SYSTEM
            filename: Optional specific filename (otherwise category.json)
            target_user_id: For admins accessing other users' data
        """
        if scope == DataScope.USER:
            # User-scoped data (sanitize to prevent path traversal)
            import re as _re

            uid = _re.sub(r"[^a-zA-Z0-9_\-.]", "_", str(target_user_id or self.user_id))
            base_dir = USERS_DIR / uid
        elif scope == DataScope.SHARED:
            base_dir = SHARED_DIR
        else:
            base_dir = DATA_DIR / "system"

        # Build path
        if category == DataCategory.HISTORY:
            # History is a directory of files
            path = base_dir / "history"
        else:
            # Other categories are JSON files
            fname = filename or f"{category.value}.json"
            path = base_dir / fname

        return DataPath(
            path=path,
            scope=scope,
            category=category,
            user_id=self.user_id if scope == DataScope.USER else None,
        )

    def resolve(
        self, category: DataCategory, scope: DataScope = DataScope.USER, filename: str = None
    ) -> DataPath:
        """
        Resolve a data path with permission check.

        Raises:
            PermissionError: If user doesn't have read access
        """
        if not self._check_permission(scope, category, write=False):
            raise PermissionError(
                f"User {self.user_id} ({self.user_role}) cannot read {scope.value}/{category.value}"
            )

        return self._get_path(category, scope, filename)

    # ---- JSON Operations ----

    def read_json(
        self, category: DataCategory, scope: DataScope = DataScope.USER, default: Any = None
    ) -> Any:
        """Read JSON data from a category."""
        data_path = self.resolve(category, scope)

        if not data_path.path.exists():
            return default if default is not None else {}

        try:
            with open(data_path.path, "r") as f:
                return json.load(f)
        except json.JSONDecodeError:
            logger.warning(f"Invalid JSON in {data_path.path}")
            return default if default is not None else {}

    def write_json(self, category: DataCategory, data: Any, scope: DataScope = DataScope.USER):
        """Write JSON data to a category."""
        if not self._check_permission(scope, category, write=True):
            raise PermissionError(
                f"User {self.user_id} ({self.user_role}) cannot write {scope.value}/{category.value}"
            )

        data_path = self._get_path(category, scope).ensure()

        from .utils import atomic_write_json

        atomic_write_json(data_path.path, data, default=str)

        logger.debug(f"Wrote {category.value} for user {self.user_id}")

    def append_json(
        self,
        category: DataCategory,
        item: Any,
        scope: DataScope = DataScope.USER,
        key: str = "items",
        max_items: int = None,
    ):
        """Append an item to a JSON array."""
        data = self.read_json(category, scope, default={key: []})

        if key not in data:
            data[key] = []

        data[key].append(item)

        # Truncate if needed
        if max_items and len(data[key]) > max_items:
            data[key] = data[key][-max_items:]

        self.write_json(category, data, scope)

    # ---- History Operations ----

    def save_history(self, conversation_id: str, messages: List[dict]):
        """Save conversation history."""
        if not self._check_permission(DataScope.USER, DataCategory.HISTORY, write=True):
            raise PermissionError("Cannot write history")

        history_dir = self.user_dir / "history"
        ensure_dir(history_dir)

        filename = f"{conversation_id}.json"
        filepath = history_dir / filename

        data = {
            "conversation_id": conversation_id,
            "user_id": self.user_id,
            "updated_at": datetime.now(timezone.utc).isoformat(),
            "messages": messages,
        }

        from .utils import atomic_write_json

        atomic_write_json(filepath, data, default=str)

    def load_history(self, conversation_id: str) -> List[dict]:
        """Load conversation history."""
        history_dir = self.user_dir / "history"
        filepath = history_dir / f"{conversation_id}.json"

        if not filepath.exists():
            return []

        try:
            with open(filepath, "r") as f:
                data = json.load(f)
                return data.get("messages", [])
        except json.JSONDecodeError:
            return []

    def list_histories(self, limit: int = 50) -> List[dict]:
        """List recent conversation histories."""
        history_dir = self.user_dir / "history"

        if not history_dir.exists():
            return []

        histories = []
        for filepath in sorted(
            history_dir.glob("*.json"), key=lambda p: p.stat().st_mtime, reverse=True
        )[:limit]:
            try:
                with open(filepath, "r") as f:
                    data = json.load(f)
                    histories.append(
                        {
                            "conversation_id": data.get("conversation_id"),
                            "updated_at": data.get("updated_at"),
                            "message_count": len(data.get("messages", [])),
                        }
                    )
            except (json.JSONDecodeError, KeyError, TypeError, IOError, OSError) as e:
                logger.debug(f"Skipping malformed history file {filepath}: {e}")
                continue

        return histories

    def delete_history(self, conversation_id: str) -> bool:
        """Delete a conversation history."""
        if not self._check_permission(DataScope.USER, DataCategory.HISTORY, write=True):
            raise PermissionError("Cannot delete history")

        filepath = self.user_dir / "history" / f"{conversation_id}.json"

        if filepath.exists():
            filepath.unlink()
            return True
        return False

    # ---- Memory Operations ----

    def get_memory(self) -> dict:
        """Get user's persistent memory."""
        return self.read_json(DataCategory.MEMORY, default={"facts": [], "preferences": {}})

    def update_memory(self, key: str, value: Any):
        """Update a memory key."""
        memory = self.get_memory()
        memory[key] = value
        self.write_json(DataCategory.MEMORY, memory)

    def add_fact(self, fact: str):
        """Add a fact to memory."""
        memory = self.get_memory()
        if "facts" not in memory:
            memory["facts"] = []
        if fact not in memory["facts"]:
            memory["facts"].append(fact)
            self.write_json(DataCategory.MEMORY, memory)

    # ---- Task Operations ----

    def get_tasks(self) -> List[dict]:
        """Get user's tasks."""
        data = self.read_json(DataCategory.TASKS, default={"tasks": []})
        return data.get("tasks", [])

    def add_task(self, task: dict):
        """Add a task."""
        self.append_json(DataCategory.TASKS, task, key="tasks")

    def update_task(self, task_id: str, updates: dict):
        """Update a task by ID."""
        data = self.read_json(DataCategory.TASKS, default={"tasks": []})
        tasks = data.get("tasks", [])

        for task in tasks:
            if task.get("id") == task_id:
                task.update(updates)
                break

        data["tasks"] = tasks
        self.write_json(DataCategory.TASKS, data)

    # ---- Settings Operations ----

    def get_settings(self) -> dict:
        """Get user's settings."""
        return self.read_json(DataCategory.SETTINGS, default={})

    def update_settings(self, updates: dict):
        """Update user settings."""
        settings = self.get_settings()
        settings.update(updates)
        self.write_json(DataCategory.SETTINGS, settings)

    # ---- Shared Data Operations ----

    def get_donors(self) -> List[dict]:
        """Get shared donor list."""
        data = self.read_json(DataCategory.DONORS, scope=DataScope.SHARED, default={"donors": []})
        return data.get("donors", [])

    def add_donor(self, donor: dict):
        """Add a donor to shared list."""
        self.append_json(DataCategory.DONORS, donor, scope=DataScope.SHARED, key="donors")

    # ---- Admin Operations ----

    def get_user_data_path(self, target_user_id: str) -> Path:
        """Admin: Get another user's data directory."""
        if self.user_role != "admin":
            raise PermissionError("Only admins can access other users' data")
        import re as _re

        safe_id = _re.sub(r"[^a-zA-Z0-9_\-.]", "_", str(target_user_id))
        return USERS_DIR / safe_id

    def get_all_user_stats(self) -> List[dict]:
        """Admin: Get storage stats for all users."""
        if self.user_role != "admin":
            raise PermissionError("Only admins can view all user stats")

        stats = []
        if not USERS_DIR.exists():
            return stats

        for user_dir in USERS_DIR.iterdir():
            if user_dir.is_dir():
                size = sum(f.stat().st_size for f in user_dir.rglob("*") if f.is_file())
                history_count = (
                    len(list((user_dir / "history").glob("*.json")))
                    if (user_dir / "history").exists()
                    else 0
                )

                stats.append(
                    {
                        "user_id": user_dir.name,
                        "size_bytes": size,
                        "size_mb": round(size / 1024 / 1024, 2),
                        "history_count": history_count,
                    }
                )

        return sorted(stats, key=lambda x: x["size_bytes"], reverse=True)

    def delete_user_data(self, target_user_id: str) -> bool:
        """Admin: Delete all data for a user."""
        if self.user_role != "admin":
            raise PermissionError("Only admins can delete user data")
        import re as _re

        safe_id = _re.sub(r"[^a-zA-Z0-9_\-.]", "_", str(target_user_id))
        user_dir = USERS_DIR / safe_id
        if user_dir.exists():
            shutil.rmtree(user_dir)
            logger.info(f"Deleted all data for user {target_user_id}")
            return True
        return False


# ============================================================
# FACTORY FUNCTION
# ============================================================


def get_data_store(user_id: str, user_role: str = "staff") -> DataStore:
    """
    Get a DataStore instance for a user.

    Args:
        user_id: The user's ID
        user_role: The user's role

    Returns:
        DataStore configured for the user
    """
    return DataStore(user_id, user_role)


def get_data_store_from_context(user_context) -> DataStore:
    """
    Get a DataStore from a UserContext object.

    Args:
        user_context: UserContext with user info

    Returns:
        DataStore configured for the user
    """
    return DataStore(user_id=user_context.user.id, user_role=user_context.user.role.value)


# ============================================================
# INITIALIZATION
# ============================================================


def init_data_structure():
    """Initialize the data directory structure."""
    ensure_dir(DATA_DIR)
    ensure_dir(SHARED_DIR)
    ensure_dir(USERS_DIR)
    ensure_dir(SHARED_DIR / "donors")
    ensure_dir(SHARED_DIR / "knowledge")
    ensure_dir(SHARED_DIR / "templates")
    ensure_dir(DATA_DIR / "system")

    logger.info(f"Initialized data structure at {DATA_DIR}")


if __name__ == "__main__":
    # Test the data store
    init_data_structure()

    # Create a test store
    store = DataStore("test_user", "staff")

    # Test user data
    store.add_fact("User likes Python")
    print(f"Memory: {store.get_memory()}")

    # Test tasks
    store.add_task({"id": "1", "title": "Test task", "done": False})
    print(f"Tasks: {store.get_tasks()}")

    print("✅ Data store working")
