# 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

"""
Skills System

Skills are plugins that extend the agent's capabilities.
Each skill is a directory containing:
  - SKILL.md - Description and usage instructions (included in system prompt)
  - skill.py - Python code defining tools (optional)
  - config.yaml - Skill configuration (optional)

Skills can:
  - Add new tools
  - Provide domain knowledge via SKILL.md
  - Define scheduled tasks
"""

import importlib.util
import logging
import os
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Optional

import yaml

from .config import SKILLS_DIR
from .tools import Tool, get_tool_registry

logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# Skill prerequisite definitions
# ---------------------------------------------------------------------------

# Maps skill name → list of (check_fn, friendly_label) tuples.
# Every check_fn() must return True for the skill to register its tools.
# Checks that fail emit a WARNING at load time so the log explains
# why a skill's tools are missing without crashing the agent.
#
# Adding a new check: append to the list for the relevant skill, or create
# a new entry. check_fn receives no arguments and returns bool.


def _env(key: str):
    """Return a check function that tests whether an env var is set."""
    return lambda: bool(os.environ.get(key, "").strip())


def _importable(module: str):
    """Return a check function that tests whether a Python module is importable."""
    import importlib

    def _check():
        try:
            return importlib.util.find_spec(module) is not None
        except (ModuleNotFoundError, ValueError):
            return False

    return _check


def _file_exists(path_str: str):
    """Return a check function that tests whether a file exists on disk."""
    return lambda: Path(path_str).expanduser().exists()


def _playwright_browsers_installed():
    """Check that playwright has at least one browser binary downloaded."""
    cache = Path.home() / ".cache" / "ms-playwright"
    if not cache.exists():
        return False
    # Look for any chromium-* directory with actual binaries
    return any(d.is_dir() and d.name.startswith("chromium") for d in cache.iterdir())


def _any_tts_engine():
    """Check that at least one TTS engine is available."""
    for mod in ("pyttsx3", "gtts", "edge_tts"):
        try:
            import importlib

            if importlib.util.find_spec(mod) is not None:
                return True
        except (ModuleNotFoundError, ValueError):
            continue
    return False


def _gmail_token() -> bool:
    """True if Gmail OAuth token exists (email/triage can use Gmail API path)."""
    p = Path.home() / ".familiar" / "data" / "google_token_gmail.json"
    return p.exists()


def _either(check_a, check_b) -> bool:
    """True if either condition is met — used for Gmail API OR IMAP/SMTP."""
    return check_a() or check_b()


# Skills with no external deps (calendar uses local JSON, tasks uses local JSON, etc.)
# are NOT listed here — absence from the dict means "always register".
SKILL_PREREQUISITES: dict[str, list[tuple]] = {
    "email": [
        # Either Gmail OAuth token exists OR IMAP/SMTP credentials are set.
        (
            lambda: (
                _gmail_token()
                or (
                    bool(os.environ.get("EMAIL_ADDRESS")) and bool(os.environ.get("EMAIL_PASSWORD"))
                )
            ),
            "Email not configured: run /connect google auth gmail  OR  set EMAIL_ADDRESS + EMAIL_PASSWORD",
        ),
    ],
    "calendar": [
        (
            lambda: (
                (_importable("google.oauth2")() and _importable("googleapiclient")() and _importable("httplib2")())
                or (_importable("caldav")() and _importable("vobject")())
            ),
            "Calendar requires Google libs (pip install familiar-agent[google]) or CalDAV libs (pip install familiar-agent[nextcloud])",
        ),
    ],
    "gdrive": [
        (
            _importable("google.oauth2"),
            "google-auth not installed (pip install google-auth google-auth-oauthlib google-auth-httplib2)",
        ),
        (_importable("googleapiclient"), "google-api-python-client not installed"),
        (_importable("httplib2"), "httplib2 not installed (pip install httplib2)"),
    ],
    "sms": [
        (_env("TWILIO_ACCOUNT_SID"), "TWILIO_ACCOUNT_SID env var not set"),
        (_env("TWILIO_AUTH_TOKEN"), "TWILIO_AUTH_TOKEN env var not set"),
        (_env("TWILIO_PHONE_NUMBER"), "TWILIO_PHONE_NUMBER env var not set"),
        (_importable("twilio"), "twilio not installed (pip install twilio)"),
    ],
    "voice": [
        (
            _importable("speech_recognition"),
            "SpeechRecognition not installed (pip install SpeechRecognition)",
        ),
        (
            _any_tts_engine,
            "No TTS engine installed (pip install pyttsx3 or pip install edge-tts)",
        ),
    ],
    "transcription": [
        (_importable("whisper"), "openai-whisper not installed (pip install openai-whisper)"),
    ],
    "gpio": [
        (
            _importable("RPi.GPIO"),
            "RPi.GPIO not installed — GPIO skill requires Raspberry Pi hardware",
        ),
    ],
    # ── Email-dependent skills ─────────────────────────────────────────────────
    # meetings, triage, and notifications all read EMAIL_ADDRESS / EMAIL_PASSWORD
    # for IMAP/SMTP access. Without them the tools fail at call time.
    "meetings": [
        # Gmail API token (for invite sending) OR SMTP credentials
        (
            lambda: (
                _gmail_token()
                or (
                    bool(os.environ.get("EMAIL_ADDRESS")) and bool(os.environ.get("EMAIL_PASSWORD"))
                )
            ),
            "Meetings not configured: run /connect google auth gmail  OR  set EMAIL_ADDRESS + EMAIL_PASSWORD",
        ),
    ],
    "triage": [
        # Gmail API token OR IMAP credentials
        (
            lambda: (
                _gmail_token()
                or (
                    bool(os.environ.get("EMAIL_ADDRESS")) and bool(os.environ.get("EMAIL_PASSWORD"))
                )
            ),
            "Triage not configured: run /connect google auth gmail  OR  set EMAIL_ADDRESS + EMAIL_PASSWORD",
        ),
    ],
    "notifications": [
        # At least one notification channel must be configured.
        # We check for email channel (most common); ntfy and Telegram are optional extras.
        # Since any channel works, we use a soft check: warn but still register
        # (notification tools gracefully skip unconfigured channels at runtime).
    ],
    "messaging": [
        # messaging detects available platforms at runtime (iMessage/WhatsApp/Signal/SMS)
        # so no hard prerequisite — detection is built into detect_available_platforms().
        # Soft note: warn if NO platform will be available.
    ],
    "triggers": [
        # Webhook secret is optional (triggers work without HMAC verification).
        # No hard prerequisite — webhook server starts on any port.
    ],
    "nextcloud": [
        (_env("NEXTCLOUD_URL"), "NEXTCLOUD_URL env var not set"),
        (_env("NEXTCLOUD_USER"), "NEXTCLOUD_USER env var not set"),
        (_env("NEXTCLOUD_TOKEN"), "NEXTCLOUD_TOKEN env var not set"),
        (_importable("caldav"), "caldav not installed (pip install familiar-agent[nextcloud])"),
        (_importable("vobject"), "vobject not installed (pip install familiar-agent[nextcloud])"),
    ],
    "browser": [
        (
            _importable("playwright.sync_api"),
            "playwright not installed (pip install playwright && playwright install)",
        ),
        (
            _playwright_browsers_installed,
            "Playwright browsers not downloaded (run: playwright install chromium)",
        ),
    ],
    "proton": [
        # No hard prerequisites — tools handle missing Bridge/VPN gracefully
        # with helpful setup messages.
    ],
    # ── Self-hosted integrations ──────────────────────────────────────────────
    "homeassistant": [
        (_env("HOMEASSISTANT_URL"), "HOMEASSISTANT_URL env var not set"),
        (_env("HOMEASSISTANT_TOKEN"), "HOMEASSISTANT_TOKEN env var not set"),
    ],
    "vaultwarden": [
        (_env("VAULTWARDEN_URL"), "VAULTWARDEN_URL env var not set"),
        (_env("VAULTWARDEN_TOKEN"), "VAULTWARDEN_TOKEN env var not set"),
    ],
    "joplin": [
        (_env("JOPLIN_TOKEN"), "JOPLIN_TOKEN env var not set"),
    ],
    "jellyfin": [
        (_env("JELLYFIN_URL"), "JELLYFIN_URL env var not set"),
        (_env("JELLYFIN_TOKEN"), "JELLYFIN_TOKEN env var not set"),
    ],
    "gitea": [
        (_env("GITEA_URL"), "GITEA_URL env var not set"),
        (_env("GITEA_TOKEN"), "GITEA_TOKEN env var not set"),
    ],
    "pihole": [
        (_env("PIHOLE_URL"), "PIHOLE_URL env var not set"),
    ],
}


def check_skill_prerequisites() -> dict[str, list[str]]:
    """Check all skill prerequisites and return a dict of {skill_name: [failures]}.

    Skills with no failures are omitted. Empty dict means everything is ready.
    Called at startup to give operators a clear picture of what works.
    """
    results = {}
    for skill_name, checks in SKILL_PREREQUISITES.items():
        failures = []
        for check_fn, label in checks:
            try:
                ok = check_fn()
            except Exception:
                ok = False
            if not ok:
                failures.append(label)
        if failures:
            results[skill_name] = failures
    return results


@dataclass
class Skill:
    """Represents a loaded skill."""

    name: str
    path: Path
    description: str  # From SKILL.md
    tools: list[Tool]
    config: dict
    enabled: bool = True


class SkillLoader:
    """
    Loads and manages skills.

    Usage:
        loader = SkillLoader(["/path/to/skills"])
        loader.load_all()

        # Get skill docs for system prompt
        docs = loader.get_skill_docs()
    """

    def __init__(self, skill_dirs: Optional[list[str]] = None):
        self.skill_dirs = [Path(d) for d in (skill_dirs or [str(SKILLS_DIR)])]
        self.skills: dict[str, Skill] = {}
        self.tool_registry = get_tool_registry()

    def discover_skills(self) -> list[Path]:
        """Find all skill directories."""
        skill_paths = []

        for skill_dir in self.skill_dirs:
            if not skill_dir.exists():
                continue

            for item in skill_dir.iterdir():
                if item.is_dir() and not item.name.startswith("_"):
                    # Check if it has SKILL.md or skill.py
                    if (item / "SKILL.md").exists() or (item / "skill.py").exists():
                        skill_paths.append(item)

        return skill_paths

    def load_skill(self, path: Path) -> Optional[Skill]:
        """Load a single skill from a directory."""
        name = path.name

        try:
            # Read description from SKILL.md
            description = ""
            skill_md = path / "SKILL.md"
            if skill_md.exists():
                description = skill_md.read_text()

            # Read config
            config = {}
            config_file = path / "config.yaml"
            if config_file.exists():
                with open(config_file) as f:
                    config = yaml.safe_load(f) or {}

            # Check if enabled
            enabled = config.get("enabled", True)

            # Load tools from skill.py
            tools = []
            skill_py = path / "skill.py"
            if skill_py.exists():
                tools = self._load_skill_module(skill_py, name)

            skill = Skill(
                name=name,
                path=path,
                description=description,
                tools=tools,
                config=config,
                enabled=enabled,
            )

            # Register tools
            if enabled:
                # Run prerequisite checks — skip registration (not the whole skill)
                # if external services are unavailable, so the skill still appears
                # in skill_docs but its tools don't inflate the LLM context.
                prereqs = SKILL_PREREQUISITES.get(name, [])
                prereq_failures = []
                for check_fn, label in prereqs:
                    try:
                        ok = check_fn()
                    except Exception:
                        ok = False
                    if not ok:
                        prereq_failures.append(label)

                if prereq_failures:
                    logger.warning(
                        f"Skill '{name}' prerequisites not met — tools will NOT be registered:\n"
                        + "\n".join(f"  • {f}" for f in prereq_failures)
                    )
                else:
                    for tool in tools:
                        self.tool_registry.register(tool)

            logger.info(f"Loaded skill: {name} ({len(tools)} tools)")
            return skill

        except Exception as e:
            logger.error(f"Failed to load skill {name}: {e}")
            return None

    def _load_skill_module(self, path: Path, skill_name: str) -> list[Tool]:
        """Load tools from a skill's Python module."""
        tools = []

        try:
            # Load module
            spec = importlib.util.spec_from_file_location(f"skill_{skill_name}", path)
            module = importlib.util.module_from_spec(spec)
            sys.modules[spec.name] = module
            spec.loader.exec_module(module)

            # Look for tools
            if hasattr(module, "TOOLS"):
                # List of Tool objects or dicts
                for t in module.TOOLS:
                    if isinstance(t, Tool):
                        tools.append(t)
                    elif isinstance(t, dict):
                        # Convert dict to Tool
                        handler = t.get("handler")
                        if handler and callable(handler):
                            tools.append(
                                Tool(
                                    name=t["name"],
                                    description=t["description"],
                                    input_schema=t["input_schema"],
                                    handler=handler,
                                    category=t.get("category", skill_name),
                                )
                            )

            # Look for register_tools function
            if hasattr(module, "register_tools"):
                additional = module.register_tools()
                if additional:
                    tools.extend(additional)

        except Exception as e:
            logger.error(f"Error loading skill module {path}: {e}")

        return tools

    def load_all(self):
        """Load all discovered skills."""
        for path in self.discover_skills():
            skill = self.load_skill(path)
            if skill:
                self.skills[skill.name] = skill

        logger.info(f"Loaded {len(self.skills)} skills")

    def get_skill(self, name: str) -> Optional[Skill]:
        """Get a skill by name."""
        return self.skills.get(name)

    def get_skill_docs(self) -> str:
        """Get all skill documentation for system prompt."""
        docs = []

        for skill in self.skills.values():
            if skill.enabled and skill.description:
                docs.append(f"## Skill: {skill.name}\n\n{skill.description}")

        return "\n\n---\n\n".join(docs)

    def enable_skill(self, name: str) -> bool:
        """Enable a skill."""
        skill = self.skills.get(name)
        if skill:
            skill.enabled = True
            for tool in skill.tools:
                self.tool_registry.register(tool)
            return True
        return False

    def disable_skill(self, name: str) -> bool:
        """Disable a skill."""
        skill = self.skills.get(name)
        if skill:
            skill.enabled = False
            for tool in skill.tools:
                self.tool_registry.unregister(tool.name)
            return True
        return False

    def list_skills(self) -> list[dict]:
        """List all skills with their status."""
        return [
            {
                "name": s.name,
                "enabled": s.enabled,
                "tools": [t.name for t in s.tools],
                "path": str(s.path),
            }
            for s in self.skills.values()
        ]


def create_skill_template(skill_dir: Path, name: str) -> Path:
    """Create a new skill from template."""
    skill_path = skill_dir / name
    skill_path.mkdir(parents=True, exist_ok=True)

    # SKILL.md
    skill_md = f"""# {name.title()} Skill

Description of what this skill does.

## Usage

Explain how the agent should use this skill.

## Tools

List the tools this skill provides.
"""
    (skill_path / "SKILL.md").write_text(skill_md)

    # skill.py
    skill_py = f'''"""
{name.title()} Skill

Custom tools for {name}.
"""

from familiar.core.tools import Tool

# Define your tools here
TOOLS = [
    # Tool(
    #     name="{name}_action",
    #     description="Description of what this tool does",
    #     input_schema={{
    #         "type": "object",
    #         "properties": {{
    #             "param": {{"type": "string", "description": "Parameter description"}}
    #         }},
    #         "required": ["param"]
    #     }},
    #     handler=lambda data: f"Result: {{data['param']}}",
    #     category="{name}"
    # )
]


def register_tools():
    """Optional: Return additional tools dynamically."""
    return []
'''
    (skill_path / "skill.py").write_text(skill_py)

    # config.yaml
    config_yaml = f"""# {name.title()} Skill Configuration
enabled: true

# Add skill-specific settings here
"""
    (skill_path / "config.yaml").write_text(config_yaml)

    logger.info(f"Created skill template: {skill_path}")
    return skill_path
