"""Basic skill provider for handling a single skill folder."""

from __future__ import annotations

import json
import mimetypes
from collections.abc import Sequence
from pathlib import Path
from typing import Any, Literal, cast

from pydantic import AnyUrl

from fastmcp.resources.base import Resource, ResourceResult
from fastmcp.resources.template import ResourceTemplate
from fastmcp.server.providers.base import Provider
from fastmcp.server.providers.skills._common import (
    SkillInfo,
    parse_frontmatter,
    scan_skill_files,
)
from fastmcp.utilities.logging import get_logger
from fastmcp.utilities.versions import VersionSpec

logger = get_logger(__name__)

# Ensure .md is recognized as text/markdown on all platforms (Windows may not have this)
mimetypes.add_type("text/markdown", ".md")


# -----------------------------------------------------------------------------
# Skill-specific Resource and ResourceTemplate subclasses
# -----------------------------------------------------------------------------


class SkillResource(Resource):
    """A resource representing a skill's main file or manifest."""

    skill_info: SkillInfo
    is_manifest: bool = False

    def get_meta(self) -> dict[str, Any]:
        meta = super().get_meta()
        fastmcp = cast(dict[str, Any], meta["fastmcp"])
        fastmcp["skill"] = {
            "name": self.skill_info.name,
            "is_manifest": self.is_manifest,
        }
        return meta

    async def read(self) -> str | bytes | ResourceResult:
        """Read the resource content."""
        if self.is_manifest:
            return self._generate_manifest()
        else:
            main_file_path = self.skill_info.path / self.skill_info.main_file
            return main_file_path.read_text(encoding="utf-8")

    def _generate_manifest(self) -> str:
        """Generate JSON manifest for the skill."""
        manifest = {
            "skill": self.skill_info.name,
            "files": [
                {"path": f.path, "size": f.size, "hash": f.hash}
                for f in self.skill_info.files
            ],
        }
        return json.dumps(manifest, indent=2)


class SkillFileTemplate(ResourceTemplate):
    """A template for accessing files within a skill."""

    skill_info: SkillInfo

    async def read(self, arguments: dict[str, Any]) -> str | bytes | ResourceResult:
        """Read a file from the skill directory."""
        file_path = arguments.get("path", "")
        full_path = self.skill_info.path / file_path

        # Security: ensure path doesn't escape skill directory
        try:
            full_path = full_path.resolve()
            if not full_path.is_relative_to(self.skill_info.path):
                raise ValueError(f"Path {file_path} escapes skill directory")
        except ValueError as e:
            raise ValueError(f"Invalid path: {e}") from e

        if not full_path.exists():
            raise FileNotFoundError(f"File not found: {file_path}")

        if not full_path.is_file():
            raise ValueError(f"Not a file: {file_path}")

        # Determine if binary or text based on mime type
        mime_type, _ = mimetypes.guess_type(str(full_path))
        if mime_type and mime_type.startswith("text/"):
            return full_path.read_text(encoding="utf-8")
        else:
            return full_path.read_bytes()

    async def _read(  # type: ignore[override]
        self,
        uri: str,
        params: dict[str, Any],
        task_meta: Any = None,
    ) -> ResourceResult:  # ty:ignore[invalid-method-override]
        """Server entry point - read file directly without creating ephemeral resource.

        Note: task_meta is ignored - this template doesn't support background tasks.
        """
        # Call read() directly and convert to ResourceResult
        result = await self.read(arguments=params)
        return self.convert_result(result)

    async def create_resource(self, uri: str, params: dict[str, Any]) -> Resource:
        """Create a resource for the given URI and parameters.

        Note: This is not typically used since _read() handles file reading directly.
        Provided for compatibility with the ResourceTemplate interface.
        """
        file_path = params.get("path", "")
        full_path = (self.skill_info.path / file_path).resolve()

        # Security: ensure path doesn't escape skill directory
        if not full_path.is_relative_to(self.skill_info.path):
            raise ValueError(f"Path {file_path} escapes skill directory")

        mime_type, _ = mimetypes.guess_type(str(full_path))

        # Create a SkillFileResource that can read the file
        return SkillFileResource(
            uri=AnyUrl(uri),
            name=f"{self.skill_info.name}/{file_path}",
            description=f"File from {self.skill_info.name} skill",
            mime_type=mime_type or "application/octet-stream",
            skill_info=self.skill_info,
            file_path=file_path,
        )


class SkillFileResource(Resource):
    """A resource representing a specific file within a skill."""

    skill_info: SkillInfo
    file_path: str

    def get_meta(self) -> dict[str, Any]:
        meta = super().get_meta()
        fastmcp = cast(dict[str, Any], meta["fastmcp"])
        fastmcp["skill"] = {
            "name": self.skill_info.name,
        }
        return meta

    async def read(self) -> str | bytes | ResourceResult:
        """Read the file content."""
        full_path = self.skill_info.path / self.file_path

        # Security check
        full_path = full_path.resolve()
        if not full_path.is_relative_to(self.skill_info.path):
            raise ValueError(f"Path {self.file_path} escapes skill directory")

        if not full_path.exists():
            raise FileNotFoundError(f"File not found: {self.file_path}")

        mime_type, _ = mimetypes.guess_type(str(full_path))
        if mime_type and mime_type.startswith("text/"):
            return full_path.read_text(encoding="utf-8")
        else:
            return full_path.read_bytes()


# -----------------------------------------------------------------------------
# SkillProvider - handles a SINGLE skill folder
# -----------------------------------------------------------------------------


class SkillProvider(Provider):
    """Provider that exposes a single skill folder as MCP resources.

    Each skill folder must contain a main file (default: SKILL.md) and may
    contain additional supporting files.

    Exposes:
    - A Resource for the main file (skill://{name}/SKILL.md)
    - A Resource for the synthetic manifest (skill://{name}/_manifest)
    - Supporting files via ResourceTemplate or Resources (configurable)

    Args:
        skill_path: Path to the skill directory.
        main_file_name: Name of the main skill file. Defaults to "SKILL.md".
        supporting_files: How supporting files (everything except main file and
            manifest) are exposed to clients:
            - "template": Accessed via ResourceTemplate, hidden from list_resources().
              Clients discover files by reading the manifest first.
            - "resources": Each file exposed as individual Resource in list_resources().
              Full enumeration upfront.

    Example:
        ```python
        from pathlib import Path
        from fastmcp import FastMCP
        from fastmcp.server.providers.skills import SkillProvider

        mcp = FastMCP("My Skill")
        mcp.add_provider(SkillProvider(
            Path.home() / ".claude/skills/pdf-processing"
        ))
        ```
    """

    def __init__(
        self,
        skill_path: str | Path,
        main_file_name: str = "SKILL.md",
        supporting_files: Literal["template", "resources"] = "template",
    ) -> None:
        super().__init__()
        self._skill_path = Path(skill_path).resolve()
        self._main_file_name = main_file_name
        self._supporting_files = supporting_files
        self._skill_info: SkillInfo | None = None

        # Load at init to catch errors early
        self._load_skill()

    def _load_skill(self) -> None:
        """Load and parse the skill directory."""
        main_file = self._skill_path / self._main_file_name

        if not self._skill_path.exists():
            raise FileNotFoundError(f"Skill directory not found: {self._skill_path}")

        if not main_file.exists():
            raise FileNotFoundError(
                f"Main skill file not found: {main_file}. "
                f"Expected {self._main_file_name} in {self._skill_path}"
            )

        content = main_file.read_text(encoding="utf-8")
        frontmatter, body = parse_frontmatter(content)

        # Get description from frontmatter or first non-empty line
        description = frontmatter.get("description", "")
        if not description:
            for line in body.strip().split("\n"):
                line = line.strip()
                if line and not line.startswith("#"):
                    description = line[:200]
                    break
                elif line.startswith("#"):
                    description = line.lstrip("#").strip()[:200]
                    break

        # Scan all files in the skill directory
        files = scan_skill_files(self._skill_path)

        self._skill_info = SkillInfo(
            name=self._skill_path.name,
            description=description or f"Skill: {self._skill_path.name}",
            path=self._skill_path,
            main_file=self._main_file_name,
            files=files,
            frontmatter=frontmatter,
        )

        logger.debug(f"SkillProvider loaded skill: {self._skill_info.name}")

    @property
    def skill_info(self) -> SkillInfo:
        """Get the loaded skill info."""
        if self._skill_info is None:
            raise RuntimeError("Skill not loaded")
        return self._skill_info

    # -------------------------------------------------------------------------
    # Provider interface implementation
    # -------------------------------------------------------------------------

    async def _list_resources(self) -> Sequence[Resource]:
        """List skill resources."""
        skill = self.skill_info
        resources: list[Resource] = []

        # Main skill file
        resources.append(
            SkillResource(
                uri=AnyUrl(f"skill://{skill.name}/{self._main_file_name}"),
                name=f"{skill.name}/{self._main_file_name}",
                description=skill.description,
                mime_type="text/markdown",
                skill_info=skill,
                is_manifest=False,
            )
        )

        # Synthetic manifest
        resources.append(
            SkillResource(
                uri=AnyUrl(f"skill://{skill.name}/_manifest"),
                name=f"{skill.name}/_manifest",
                description=f"File listing for {skill.name}",
                mime_type="application/json",
                skill_info=skill,
                is_manifest=True,
            )
        )

        # If supporting_files="resources", add all supporting files as resources
        if self._supporting_files == "resources":
            for file_info in skill.files:
                # Skip main file and manifest (already added)
                if file_info.path == self._main_file_name:
                    continue

                mime_type, _ = mimetypes.guess_type(file_info.path)
                resources.append(
                    SkillFileResource(
                        uri=AnyUrl(f"skill://{skill.name}/{file_info.path}"),
                        name=f"{skill.name}/{file_info.path}",
                        description=f"File from {skill.name} skill",
                        mime_type=mime_type or "application/octet-stream",
                        skill_info=skill,
                        file_path=file_info.path,
                    )
                )

        return resources

    async def _get_resource(
        self, uri: str, version: VersionSpec | None = None
    ) -> Resource | None:
        """Get a resource by URI."""
        skill = self.skill_info

        # Parse URI: skill://{skill_name}/{file_path}
        if not uri.startswith("skill://"):
            return None

        path_part = uri[len("skill://") :]
        parts = path_part.split("/", 1)
        if len(parts) != 2:
            return None

        skill_name, file_path = parts
        if skill_name != skill.name:
            return None

        if file_path == "_manifest":
            return SkillResource(
                uri=AnyUrl(uri),
                name=f"{skill_name}/_manifest",
                description=f"File listing for {skill_name}",
                mime_type="application/json",
                skill_info=skill,
                is_manifest=True,
            )
        elif file_path == self._main_file_name:
            return SkillResource(
                uri=AnyUrl(uri),
                name=f"{skill_name}/{self._main_file_name}",
                description=skill.description,
                mime_type="text/markdown",
                skill_info=skill,
                is_manifest=False,
            )
        elif self._supporting_files == "resources":
            # Check if it's a known supporting file
            for file_info in skill.files:
                if file_info.path == file_path:
                    mime_type, _ = mimetypes.guess_type(file_path)
                    return SkillFileResource(
                        uri=AnyUrl(uri),
                        name=f"{skill_name}/{file_path}",
                        description=f"File from {skill_name} skill",
                        mime_type=mime_type or "application/octet-stream",
                        skill_info=skill,
                        file_path=file_path,
                    )

        return None

    async def _list_resource_templates(self) -> Sequence[ResourceTemplate]:
        """List resource templates for accessing files within the skill."""
        # Only expose template if supporting_files="template"
        if self._supporting_files != "template":
            return []

        skill = self.skill_info
        return [
            SkillFileTemplate(
                uri_template=f"skill://{skill.name}/{{path*}}",
                name=f"{skill.name}_files",
                description=f"Access files within {skill.name}",
                mime_type="application/octet-stream",
                parameters={
                    "type": "object",
                    "properties": {"path": {"type": "string"}},
                    "required": ["path"],
                },
                skill_info=skill,
            )
        ]

    async def _get_resource_template(
        self, uri: str, version: VersionSpec | None = None
    ) -> ResourceTemplate | None:
        """Get a resource template that matches the given URI."""
        # Only match if supporting_files="template"
        if self._supporting_files != "template":
            return None

        skill = self.skill_info

        if not uri.startswith("skill://"):
            return None

        path_part = uri[len("skill://") :]
        parts = path_part.split("/", 1)
        if len(parts) != 2:
            return None

        skill_name, file_path = parts
        if skill_name != skill.name:
            return None

        # Don't match known resources (main file, manifest)
        if file_path == "_manifest" or file_path == self._main_file_name:
            return None

        return SkillFileTemplate(
            uri_template=f"skill://{skill.name}/{{path*}}",
            name=f"{skill.name}_files",
            description=f"Access files within {skill.name}",
            mime_type="application/octet-stream",
            parameters={
                "type": "object",
                "properties": {"path": {"type": "string"}},
                "required": ["path"],
            },
            skill_info=skill,
        )

    def __repr__(self) -> str:
        return (
            f"SkillProvider(skill_path={self._skill_path!r}, "
            f"supporting_files={self._supporting_files!r})"
        )
