"""Skill management tool — create, patch, edit, delete, and list skills.

Skills are the agent's procedural memory: reusable approaches saved as
markdown files (SKILL.md) under ``~/.flowly/skills/{name}/``.

All write operations:
  1. Validate the skill name (lowercase, hyphens/underscores, max 64 chars).
  2. Validate YAML frontmatter (``description`` field required for create/edit).
  3. Run ``content_guard.scan_content()`` to block prompt-injection payloads.
  4. Use atomic writes (temp file + ``os.replace``) to prevent corruption.
  5. Roll back on security-scan failure.
"""

from __future__ import annotations

import os
import re
import tempfile
from pathlib import Path
from typing import Any

from flowly.agent.tools.base import Tool
from flowly.agent.tools.content_guard import scan_content

# Where agent-created skills live
def _skills_dir() -> Path:
    from flowly.profile import get_flowly_home
    return get_flowly_home() / "skills"

_NAME_RE = re.compile(r"^[a-z0-9][a-z0-9_-]{0,63}$")


# ── Helpers ────────────────────────────────────────────────────────


def _validate_name(name: str) -> str | None:
    """Return error string if name is invalid, else None."""
    if not name:
        return "Skill name is required."
    if not _NAME_RE.match(name):
        return (
            f"Invalid skill name '{name}'. "
            "Use lowercase letters, digits, hyphens, underscores (max 64 chars). "
            "Must start with a letter or digit."
        )
    return None


def _validate_frontmatter(content: str) -> str | None:
    """Return error string if SKILL.md frontmatter is missing/broken."""
    if not content.startswith("---"):
        return "SKILL.md must start with YAML frontmatter (---). Example:\n---\ndescription: \"What the skill does\"\n---"
    match = re.match(r"^---\n(.*?)\n---", content, re.DOTALL)
    if not match:
        return "SKILL.md frontmatter is not closed (missing second ---)."
    fm = match.group(1)
    if "description" not in fm:
        return "SKILL.md frontmatter must include a 'description' field."
    return None


def _find_skill(name: str) -> Path | None:
    """Find an existing skill directory by name across all locations."""
    # Managed/agent-created skills
    candidate = _skills_dir() / name
    if candidate.is_dir() and (candidate / "SKILL.md").exists():
        return candidate
    # Workspace skills would be at workspace/skills/{name} but we don't
    # know workspace here — callers should check workspace separately.
    return None


def _atomic_write(path: Path, content: str) -> None:
    """Write content atomically via temp file + os.replace."""
    path.parent.mkdir(parents=True, exist_ok=True)
    fd, tmp = tempfile.mkstemp(
        dir=str(path.parent),
        prefix=f".{path.name}.tmp.",
    )
    try:
        with os.fdopen(fd, "w", encoding="utf-8") as f:
            f.write(content)
        os.replace(tmp, str(path))
    except Exception:
        try:
            os.unlink(tmp)
        except OSError:
            pass
        raise


# ── Tool class ─────────────────────────────────────────────────────


class SkillManageTool(Tool):
    """Tool for creating, updating, and deleting agent skills."""

    def __init__(self, workspace: Path | None = None):
        self._workspace = workspace

    @property
    def name(self) -> str:
        return "skill_manage"

    @property
    def description(self) -> str:
        return (
            "Manage skills (create, patch, edit, delete, list). "
            "Skills are your procedural memory — reusable approaches for recurring tasks. "
            "Create when: complex task succeeded (5+ tool calls), tricky error fixed, "
            "non-trivial workflow discovered. "
            "Patch when: skill instructions are stale, incomplete, or wrong. "
            "If you used a skill and hit issues not covered by it, patch it immediately."
        )

    @property
    def parameters(self) -> dict[str, Any]:
        return {
            "type": "object",
            "properties": {
                "action": {
                    "type": "string",
                    "enum": ["create", "patch", "edit", "delete", "list"],
                    "description": "The action to perform.",
                },
                "name": {
                    "type": "string",
                    "description": (
                        "Skill name (lowercase, hyphens/underscores, max 64 chars). "
                        "Required for all actions except 'list'."
                    ),
                },
                "content": {
                    "type": "string",
                    "description": (
                        "Full SKILL.md content (YAML frontmatter + markdown body). "
                        "Required for 'create' and 'edit'."
                    ),
                },
                "old_string": {
                    "type": "string",
                    "description": "Text to find in SKILL.md (required for 'patch').",
                },
                "new_string": {
                    "type": "string",
                    "description": "Replacement text (required for 'patch').",
                },
            },
            "required": ["action"],
        }

    async def execute(self, action: str, **kwargs: Any) -> str:
        if action == "list":
            return self._list()
        if action == "create":
            return self._create(kwargs.get("name", ""), kwargs.get("content", ""))
        if action == "patch":
            return self._patch(
                kwargs.get("name", ""),
                kwargs.get("old_string", ""),
                kwargs.get("new_string", ""),
            )
        if action == "edit":
            return self._edit(kwargs.get("name", ""), kwargs.get("content", ""))
        if action == "delete":
            return self._delete(kwargs.get("name", ""))
        return f"Error: Unknown action '{action}'. Use: create, patch, edit, delete, list."

    # ── Actions ────────────────────────────────────────────────────

    def _list(self) -> str:
        """List all agent-created skills."""
        _skills_dir().mkdir(parents=True, exist_ok=True)
        skills = []
        for d in sorted(_skills_dir().iterdir()):
            if d.is_dir() and (d / "SKILL.md").exists():
                # Read first line of description from frontmatter
                desc = ""
                try:
                    text = (d / "SKILL.md").read_text(encoding="utf-8")
                    m = re.search(r"description:\s*[\"']?(.+?)[\"']?\s*$", text, re.MULTILINE)
                    if m:
                        desc = m.group(1)
                except Exception:
                    pass
                skills.append(f"- {d.name}: {desc}" if desc else f"- {d.name}")

        if not skills:
            return "No agent-created skills found. Use action='create' to make one."
        return "Agent-created skills:\n" + "\n".join(skills)

    def _create(self, name: str, content: str) -> str:
        """Create a new skill."""
        err = _validate_name(name)
        if err:
            return f"Error: {err}"

        if not content:
            return "Error: 'content' is required for create. Provide full SKILL.md content."

        err = _validate_frontmatter(content)
        if err:
            return f"Error: {err}"

        # Check name collision
        existing = _find_skill(name)
        if existing:
            return f"Error: Skill '{name}' already exists at {existing}. Use 'patch' or 'edit' to update."

        # Security scan
        violation = scan_content(content)
        if violation:
            return f"Error: {violation}"

        # Create
        skill_dir = _skills_dir() / name
        skill_md = skill_dir / "SKILL.md"
        _atomic_write(skill_md, content)

        return f"Skill '{name}' created at {skill_dir}."

    def _patch(self, name: str, old_string: str, new_string: str) -> str:
        """Patch a skill with targeted find-replace."""
        err = _validate_name(name)
        if err:
            return f"Error: {err}"

        if not old_string:
            return "Error: 'old_string' is required for patch."
        if new_string is None:
            return "Error: 'new_string' is required for patch (use empty string to delete text)."

        existing = _find_skill(name)
        if not existing:
            return f"Error: Skill '{name}' not found. Use 'list' to see available skills."

        skill_md = existing / "SKILL.md"
        original = skill_md.read_text(encoding="utf-8")

        count = original.count(old_string)
        if count == 0:
            preview = original[:400] + ("..." if len(original) > 400 else "")
            return f"Error: old_string not found in SKILL.md.\n\nFile preview:\n{preview}"
        if count > 1:
            return f"Error: old_string matched {count} times. Provide more context to make it unique."

        new_content = original.replace(old_string, new_string, 1)

        # Validate frontmatter still intact
        err = _validate_frontmatter(new_content)
        if err:
            return f"Error: Patch would break SKILL.md structure: {err}"

        # Security scan on new content
        violation = scan_content(new_content)
        if violation:
            return f"Error: {violation}"

        _atomic_write(skill_md, new_content)
        return f"Skill '{name}' patched successfully."

    def _edit(self, name: str, content: str) -> str:
        """Full rewrite of a skill's SKILL.md."""
        err = _validate_name(name)
        if err:
            return f"Error: {err}"

        if not content:
            return "Error: 'content' is required for edit. Provide full SKILL.md content."

        err = _validate_frontmatter(content)
        if err:
            return f"Error: {err}"

        existing = _find_skill(name)
        if not existing:
            return f"Error: Skill '{name}' not found. Use 'create' for new skills."

        # Security scan
        violation = scan_content(content)
        if violation:
            return f"Error: {violation}"

        skill_md = existing / "SKILL.md"
        _atomic_write(skill_md, content)
        return f"Skill '{name}' updated."

    def _delete(self, name: str) -> str:
        """Delete a skill directory."""
        err = _validate_name(name)
        if err:
            return f"Error: {err}"

        existing = _find_skill(name)
        if not existing:
            return f"Error: Skill '{name}' not found."

        import shutil
        shutil.rmtree(existing)
        return f"Skill '{name}' deleted."
