# 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

"""
Skill Data Utilities

Standardized data persistence for skills. Use these instead of
implementing custom load/save functions in each skill.

Usage:
    from familiar.core.skill_data import load_skill_data, save_skill_data

    # Load data (returns empty dict if file doesn't exist)
    data = load_skill_data("my_skill")

    # Modify data
    data["key"] = "value"

    # Save atomically (crash-safe)
    save_skill_data("my_skill", data)

    # Or use the context manager for automatic save
    with skill_data("my_skill") as data:
        data["key"] = "value"
    # Automatically saved on exit
"""

import json
import logging
from contextlib import contextmanager
from pathlib import Path
from typing import Any, Dict, Optional

from .utils import atomic_write_json

logger = logging.getLogger(__name__)


def _get_data_dir() -> Path:
    """Get the data directory (resolved at runtime for testing)."""
    # Import here to allow mocking in tests
    from .paths import DATA_DIR, ensure_dir

    ensure_dir(DATA_DIR)
    return DATA_DIR


def get_skill_data_path(skill_name: str) -> Path:
    """Get the data file path for a skill."""
    # Sanitize skill name to prevent path traversal
    safe_name = "".join(c for c in skill_name if c.isalnum() or c in "_-")
    if not safe_name:
        raise ValueError(f"Invalid skill name: {skill_name}")
    return _get_data_dir() / f"{safe_name}.json"


def load_skill_data(skill_name: str, default: Optional[Dict] = None) -> Dict[str, Any]:
    """
    Load persistent data for a skill.

    Args:
        skill_name: Name of the skill (used as filename)
        default: Default value if file doesn't exist (default: empty dict)

    Returns:
        The loaded data dictionary

    Raises:
        json.JSONDecodeError: If file exists but contains invalid JSON
    """
    path = get_skill_data_path(skill_name)

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

    try:
        with open(path, "r", encoding="utf-8") as f:
            data = json.load(f)
            logger.debug(f"Loaded skill data from {path}")
            return data
    except json.JSONDecodeError as e:
        logger.error(f"Invalid JSON in {path}: {e}")
        # Try backup file
        backup = path.with_suffix(".json.bak")
        if backup.exists():
            try:
                with open(backup, "r", encoding="utf-8") as f:
                    data = json.load(f)
                    logger.warning(f"Loaded skill data from backup {backup}")
                    return data
            except (json.JSONDecodeError, IOError):
                pass
        raise
    except IOError as e:
        logger.error(f"Failed to read {path}: {e}")
        raise


def save_skill_data(skill_name: str, data: Dict[str, Any], indent: int = 2) -> None:
    """
    Save persistent data for a skill (atomic, crash-safe).

    Args:
        skill_name: Name of the skill (used as filename)
        data: Dictionary to save
        indent: JSON indentation (use None for compact)
    """
    path = get_skill_data_path(skill_name)
    # ensure parent exists (get_skill_data_path calls _get_data_dir which ensures it)

    # Create backup of existing file
    if path.exists():
        backup = path.with_suffix(".json.bak")
        try:
            import shutil

            shutil.copy2(path, backup)
        except IOError:
            pass  # Backup is best-effort

    atomic_write_json(path, data, indent=indent)
    logger.debug(f"Saved skill data to {path}")


@contextmanager
def skill_data(skill_name: str, default: Optional[Dict] = None):
    """
    Context manager for skill data that auto-saves on exit.

    Usage:
        with skill_data("my_skill") as data:
            data["count"] = data.get("count", 0) + 1
        # Automatically saved

    Args:
        skill_name: Name of the skill
        default: Default value if file doesn't exist

    Yields:
        The data dictionary (modifications are saved on exit)
    """
    data = load_skill_data(skill_name, default)
    try:
        yield data
    finally:
        save_skill_data(skill_name, data)


def delete_skill_data(skill_name: str) -> bool:
    """
    Delete a skill's data file.

    Args:
        skill_name: Name of the skill

    Returns:
        True if file was deleted, False if it didn't exist
    """
    path = get_skill_data_path(skill_name)
    if path.exists():
        path.unlink()
        logger.info(f"Deleted skill data: {path}")
        return True
    return False


def list_skill_data() -> list[str]:
    """
    List all skills that have stored data.

    Returns:
        List of skill names with data files
    """
    data_dir = _get_data_dir()
    if not data_dir.exists():
        return []

    return [
        p.stem
        for p in data_dir.glob("*.json")
        if not p.name.startswith(".")
        and p.stem not in ("memory", "history", "scheduled_tasks")  # Skip core files
    ]
