import pydantic.json

import abc
import asyncio
import json
from pathlib import Path
from typing import Dict, Optional, Callable, Any, Union, Awaitable

import httpx
from pydantic import BaseModel, field_validator
from pydantic.networks import _BaseUrl

from .utilities.logging import get_logger

logger = get_logger(__name__)


class Resource(BaseModel):
    """Base class for all resources.

    Resources can contain either text (UTF-8 encoded) or binary data.
    Text resources are suitable for source code, logs, JSON, etc.
    Binary resources are suitable for images, PDFs, audio, etc.
    """

    uri: _BaseUrl
    name: str
    description: Optional[str] = None
    mime_type: Optional[str] = None
    is_binary: bool = False

    @field_validator("name", mode="before")
    @classmethod
    def set_default_name(cls, name: str | None, info) -> str:
        """Set default name from URI if not provided."""
        if name is not None:
            return name
        # Extract everything after the protocol (e.g., "desktop" from "resource://desktop")
        uri = info.data.get("uri")
        if uri:
            return str(uri).split("://", 1)[1]
        raise ValueError("Either name or uri must be provided")

    @abc.abstractmethod
    async def read(self) -> Union[str, bytes]:
        """Read the resource content.

        Returns:
            Union[str, bytes]: Text content as str for text resources,
                             binary content as bytes for binary resources
        """
        return ""


class FunctionResource(Resource):
    """A resource that is generated by a function call.

    The function can be sync or async and must return a string, bytes,
    or another Resource.
    """

    func: Union[Callable[[], Any], Callable[[], Awaitable[Any]]]
    is_async: bool = False

    def __init__(self, **data):
        super().__init__(**data)
        self.is_async = asyncio.iscoroutinefunction(self.func)

    async def read(self) -> Union[str, bytes]:
        """Read the resource content by calling the function."""
        try:
            result = (
                await self.func()
                if self.is_async
                else await asyncio.to_thread(self.func)
            )

            if isinstance(result, Resource):
                return await result.read()
            if isinstance(result, bytes):
                return result
            if not isinstance(result, str):
                try:
                    return json.dumps(result, default=pydantic.json.pydantic_encoder)
                except json.JSONDecodeError:
                    return str(result)
            return result
        except Exception as e:
            raise ValueError(f"Error calling function {self.func.__name__}: {e}")


class FileResource(Resource):
    """A file resource."""

    path: Path

    @field_validator("path")
    @classmethod
    def validate_absolute_path(cls, path: Path) -> Path:
        """Ensure path is absolute."""
        if not path.is_absolute():
            raise ValueError(f"Path must be absolute: {path}")
        return path

    async def read(self) -> Union[str, bytes]:
        """Read the file content."""
        try:
            if self.is_binary:
                return await asyncio.to_thread(self.path.read_bytes)
            return await asyncio.to_thread(self.path.read_text)
        except FileNotFoundError:
            raise FileNotFoundError(f"File not found: {self.path}")
        except PermissionError:
            raise PermissionError(f"Permission denied: {self.path}")
        except Exception as e:
            raise ValueError(f"Error reading file {self.path}: {e}")


class HttpResource(Resource):
    """An HTTP resource."""

    url: str
    headers: Optional[Dict[str, str]] = None

    async def read(self) -> Union[str, bytes]:
        """Read the HTTP resource content."""
        try:
            async with httpx.AsyncClient() as client:
                response = await client.get(self.url, headers=self.headers)
                response.raise_for_status()
                return response.content if self.is_binary else response.text
        except httpx.HTTPStatusError as e:
            raise ValueError(f"HTTP error {e.response.status_code}: {e}")
        except httpx.RequestError as e:
            raise ValueError(f"Request failed: {e}")


class DirectoryResource(Resource):
    """A directory resource."""

    path: Path
    recursive: bool = False
    pattern: Optional[str] = None
    mime_type: Optional[str] = "application/json"

    @field_validator("path")
    @classmethod
    def validate_absolute_path(cls, path: Path) -> Path:
        """Ensure path is absolute."""
        if not path.is_absolute():
            raise ValueError(f"Path must be absolute: {path}")
        return path

    def list_files(self) -> list[Path]:
        """List files in the directory."""
        if not self.path.exists():
            raise FileNotFoundError(f"Directory not found: {self.path}")
        if not self.path.is_dir():
            raise NotADirectoryError(f"Not a directory: {self.path}")

        try:
            if self.pattern:
                return (
                    list(self.path.glob(self.pattern))
                    if not self.recursive
                    else list(self.path.rglob(self.pattern))
                )
            return (
                list(self.path.glob("*"))
                if not self.recursive
                else list(self.path.rglob("*"))
            )
        except Exception as e:
            raise ValueError(f"Error listing directory {self.path}: {e}")

    async def read(self) -> str:  # Always returns JSON string
        """Read the directory listing."""
        try:
            files = await asyncio.to_thread(self.list_files)
            file_list = [str(f.relative_to(self.path)) for f in files if f.is_file()]
            return json.dumps({"files": file_list}, indent=2)
        except Exception as e:
            raise ValueError(f"Error reading directory {self.path}: {e}")


class ResourceManager:
    """Manages FastMCP resources."""

    def __init__(self, warn_on_duplicate_resources: bool = True):
        self._resources: Dict[str, Resource] = {}
        self.warn_on_duplicate_resources = warn_on_duplicate_resources

    def get_resource(self, uri: Union[_BaseUrl, str]) -> Optional[Resource]:
        """Get resource by URI."""
        uri = str(uri)
        logger.debug("Getting resource", extra={"uri": uri})

        if resource := self._resources.get(uri):
            return resource

        raise ValueError(f"Unknown resource: {uri}")

    def list_resources(self) -> list[Resource]:
        """List all registered resources."""
        logger.debug("Listing resources", extra={"count": len(self._resources)})
        return list(self._resources.values())

    def add_resource(self, resource: Resource) -> Resource:
        """Add a resource to the manager.

        Args:
            resource: A Resource instance to add

        Returns:
            The added resource. If a resource with the same URI already exists,
            returns the existing resource.
        """
        logger.debug(
            "Adding resource",
            extra={
                "uri": resource.uri,
                "type": type(resource).__name__,
                "name": resource.name,
            },
        )
        existing = self._resources.get(str(resource.uri))
        if existing:
            if self.warn_on_duplicate_resources:
                logger.warning(f"Resource already exists: {resource.uri}")
            return existing
        self._resources[str(resource.uri)] = resource
        return resource
