"""Knowledge Base tool — manage projects, documents, sources and search."""

from __future__ import annotations

import logging
from pathlib import Path

from .._api.client import (
    AsyncBaseResource,
    BaseResource,
)
from .._config import SDKConfig
from ..exceptions import api_error_handler, async_api_error_handler

logger = logging.getLogger(__name__)

from .._api.generated.knowbase.knowbase__api__knowbase.sync_client import (
    SyncKnowbaseKnowbaseAPI as _SyncAPI,
)
from .._api.generated.knowbase.knowbase__api__knowbase.client import (
    KnowbaseKnowbaseAPI as _AsyncAPI,
)
from .._api.generated.knowbase.enums import DataSourceSourceType
from .._api.generated.knowbase.knowbase__api__knowbase.models import (
    Project,
    ProjectCreate,
    ProjectCreateRequest,
    PatchedProjectRequest,
    PaginatedProjectList,
    Document,
    DocumentUploadRequest,
    PaginatedDocumentList,
    DataSource,
    DataSourceCreateRequest,
    DataSourceRequest,
    PatchedDataSourceRequest,
    PaginatedDataSourceList,
    SearchRequestRequest,
    SearchResponse,
)


# ── Sync resource ────────────────────────────────────────────────────────────

class KnowledgeBaseResource(BaseResource):
    """Knowledge Base tool (sync).

    Manage KB projects, upload documents, configure GitHub sources, and search.

    Usage::

        client = SDKRouter(api_key="sk_live_xxx")

        # Create a project
        project = client.knowbase.projects.create(name="My Docs", slug="my-docs")

        # Upload a document
        doc = client.knowbase.documents("my-docs").upload(
            title="API Guide", content="# API Guide\\n...",
        )

        # Add a GitHub source and trigger crawl
        source = client.knowbase.sources("my-docs").add(url="https://github.com/org/repo")
        client.knowbase.sources("my-docs").crawl(source.id)

        # Search
        results = client.knowbase.search("my-docs", "how to authenticate")
    """

    def __init__(self, config: SDKConfig) -> None:
        super().__init__(config)
        self._api = _SyncAPI(self._http_client)
        self._projects: _ProjectsResource | None = None
        self._documents: dict[str, _DocumentsResource] = {}
        self._sources: dict[str, _SourcesResource] = {}

    @property
    def projects(self) -> _ProjectsResource:
        """CRUD for KB projects."""
        if self._projects is None:
            self._projects = _ProjectsResource(self._config)
        return self._projects

    def documents(self, project_slug: str) -> _DocumentsResource:
        """Documents resource scoped to a project."""
        if project_slug not in self._documents:
            self._documents[project_slug] = _DocumentsResource(self._config, project_slug)
        return self._documents[project_slug]

    def sources(self, project_slug: str) -> _SourcesResource:
        """Data sources resource scoped to a project."""
        if project_slug not in self._sources:
            self._sources[project_slug] = _SourcesResource(self._config, project_slug)
        return self._sources[project_slug]

    @api_error_handler
    def search(
        self,
        project_slug: str,
        query: str,
        *,
        limit: int = 10,
        threshold: float | None = None,
    ) -> SearchResponse:
        """Semantic vector search within a knowledge base project.

        Args:
            project_slug: Project identifier slug.
            query: Search query text (any language).
            limit: Max number of results to return (default: 10).
            threshold: Minimum similarity score 0.0–1.0 (uses project default if None).

        Returns:
            SearchResponse with ranked results, token count, and timing.
        """
        logger.debug("KB search: project=%s query=%r limit=%d", project_slug, query, limit)
        data = SearchRequestRequest(query=query, limit=limit, threshold=threshold)
        result = self._api.projects_search_create(project_slug=project_slug, data=data)
        logger.info("KB search: project=%s found=%d", project_slug, result.total)
        return result


class _ProjectsResource(BaseResource):
    """CRUD for Knowledge Base projects (sync)."""

    def __init__(self, config: SDKConfig) -> None:
        super().__init__(config)
        self._api = _SyncAPI(self._http_client)

    @api_error_handler
    def list(self, *, page: int = 1, page_size: int = 20) -> PaginatedProjectList:
        """List your KB projects."""
        return self._api.projects_list(page=page, page_size=page_size)  # type: ignore[return-value]

    @api_error_handler
    def create(
        self,
        *,
        name: str,
        slug: str,
        description: str | None = None,
        is_public: bool = False,
        embedding_model: str | None = None,
        similarity_threshold: float | None = None,
        max_search_results: int | None = None,
    ) -> ProjectCreate:
        """Create a new KB project.

        Args:
            name: Display name.
            slug: URL-safe identifier (unique per user).
            description: Optional description.
            is_public: If True, accessible via MCP without auth.
            embedding_model: Override embedding model (default: text-embedding-ada-002).
            similarity_threshold: Min cosine similarity 0.0–1.0.
            max_search_results: Hard cap on search results.
        """
        logger.debug("KB project create: slug=%s", slug)
        data = ProjectCreateRequest(
            name=name,
            slug=slug,
            description=description,
            is_public=is_public,
            embedding_model=embedding_model,
            similarity_threshold=similarity_threshold,
            max_search_results=max_search_results,
        )
        return self._api.projects_create(data=data)

    @api_error_handler
    def get(self, slug: str) -> Project:
        """Get a project by slug."""
        return self._api.projects_retrieve(slug=slug)

    @api_error_handler
    def update(
        self,
        slug: str,
        *,
        name: str | None = None,
        description: str | None = None,
        is_public: bool | None = None,
        similarity_threshold: float | None = None,
        max_search_results: int | None = None,
    ) -> Project:
        """Partially update a project."""
        data = PatchedProjectRequest(  # type: ignore[call-arg]
            name=name,
            description=description,
            is_public=is_public,
            similarity_threshold=similarity_threshold,
            max_search_results=max_search_results,
        )
        return self._api.projects_partial_update(slug=slug, data=data)

    @api_error_handler
    def delete(self, slug: str) -> None:
        """Delete a project and all its documents, chunks, and sources."""
        logger.info("KB project delete: slug=%s", slug)
        self._api.projects_destroy(slug=slug)


class _DocumentsResource(BaseResource):
    """Documents within a KB project (sync)."""

    def __init__(self, config: SDKConfig, project_slug: str) -> None:
        super().__init__(config, use_upload_timeout=True)
        self._api = _SyncAPI(self._http_client)
        self._slug = project_slug

    @api_error_handler
    def list(self, *, page: int = 1, page_size: int = 20) -> PaginatedDocumentList:
        """List documents in this project."""
        return self._api.projects_documents_list(  # type: ignore[return-value]
            project_slug=self._slug, page=page, page_size=page_size,
        )

    @api_error_handler
    def upload(
        self,
        *,
        title: str,
        content: str,
        source_url: str | None = None,
    ) -> Document:
        """Upload a document (Markdown/plain text).

        The document will be chunked and embedded asynchronously in the background.

        Args:
            title: Document title.
            content: Raw Markdown or plain text content.
            source_url: Optional original URL (e.g. link to the source page).
        """
        logger.debug("KB document upload: project=%s title=%r", self._slug, title)
        data = DocumentUploadRequest(title=title, content=content, source_url=source_url)
        result = self._api.projects_documents_create(project_slug=self._slug, data=data)
        logger.info("KB document uploaded: project=%s id=%s status=%s", self._slug, result.id, result.processing_status)
        return result

    @api_error_handler
    def upload_file(self, path: Path, *, source_url: str | None = None) -> Document:
        """Upload a local Markdown or text file.

        Args:
            path: Path to a .md or .txt file.
            source_url: Optional URL to associate with this document.
        """
        path = Path(path)
        content = path.read_text(encoding="utf-8")
        title = path.stem.replace("-", " ").replace("_", " ").title()
        logger.debug("KB document upload_file: project=%s path=%s", self._slug, path)
        return self.upload(title=title, content=content, source_url=source_url)

    @api_error_handler
    def get(self, document_id: str) -> Document:
        """Get a document by ID."""
        return self._api.projects_documents_retrieve(id=document_id, project_slug=self._slug)

    @api_error_handler
    def delete(self, document_id: str) -> None:
        """Delete a document and all its chunks."""
        logger.info("KB document delete: project=%s id=%s", self._slug, document_id)
        self._api.projects_documents_destroy(id=document_id, project_slug=self._slug)


class _SourcesResource(BaseResource):
    """GitHub data sources for a KB project (sync)."""

    def __init__(self, config: SDKConfig, project_slug: str) -> None:
        super().__init__(config)
        self._api = _SyncAPI(self._http_client)
        self._slug = project_slug

    @api_error_handler
    def list(self, *, page: int = 1, page_size: int = 20) -> PaginatedDataSourceList:
        """List data sources for this project."""
        return self._api.projects_sources_list(  # type: ignore[return-value]
            project_slug=self._slug, page=page, page_size=page_size,
        )

    @api_error_handler
    def add(
        self,
        *,
        url: str,
        branch: str = "main",
        path_filter: str | None = None,
        crawl_interval_hours: int = 24,
    ) -> DataSource:
        """Add a GitHub repository as a data source.

        Args:
            url: GitHub repository URL, e.g. ``https://github.com/org/repo``.
            branch: Branch, tag or commit SHA to crawl (default: main).
            path_filter: Optional subdirectory filter, e.g. ``docs/``.
            crawl_interval_hours: How often to re-crawl (default: 24h).
        """
        logger.debug("KB source add: project=%s url=%s", self._slug, url)
        data = DataSourceCreateRequest(
            source_type=DataSourceSourceType.GITHUB_REPO,
            url=url,
            branch=branch,
            path_filter=path_filter,
            crawl_interval_hours=crawl_interval_hours,
        )
        result = self._api.projects_sources_create(project_slug=self._slug, data=data)
        logger.info("KB source added: project=%s", self._slug)
        return result  # type: ignore[return-value]

    @api_error_handler
    def get(self, source_id: str) -> DataSource:
        """Get a source by ID."""
        return self._api.projects_sources_retrieve(id=source_id, project_slug=self._slug)

    @api_error_handler
    def update(
        self,
        source_id: str,
        *,
        branch: str | None = None,
        path_filter: str | None = None,
        crawl_interval_hours: int | None = None,
        is_active: bool | None = None,
    ) -> DataSource:
        """Update a data source."""
        data = PatchedDataSourceRequest(  # type: ignore[call-arg]
            branch=branch,
            path_filter=path_filter,
            crawl_interval_hours=crawl_interval_hours,
            is_active=is_active,
        )
        return self._api.projects_sources_partial_update(
            id=source_id, project_slug=self._slug, data=data,
        )

    @api_error_handler
    def delete(self, source_id: str) -> None:
        """Remove a data source (does not delete already-ingested documents)."""
        logger.info("KB source delete: project=%s id=%s", self._slug, source_id)
        self._api.projects_sources_destroy(id=source_id, project_slug=self._slug)

    @api_error_handler
    def crawl(self, source_id: str) -> None:
        """Trigger an immediate crawl of this data source (background RQ job)."""
        logger.info("KB source crawl: project=%s id=%s", self._slug, source_id)
        source = self.get(source_id)
        crawl_data = DataSourceRequest(
            source_type=source.source_type,
            url=source.url,
            branch=source.branch,
            path_filter=source.path_filter,
            crawl_interval_hours=source.crawl_interval_hours,
            is_active=source.is_active,
        )
        self._api.projects_sources_crawl_create(
            id=source_id, project_slug=self._slug, data=crawl_data,
        )


# ── Async resource ───────────────────────────────────────────────────────────

class AsyncKnowledgeBaseResource(AsyncBaseResource):
    """Knowledge Base tool (async).

    Async equivalent of :class:`KnowledgeBaseResource`.
    """

    def __init__(self, config: SDKConfig) -> None:
        super().__init__(config)
        self._api = _AsyncAPI(self._http_client)
        self._projects: _AsyncProjectsResource | None = None
        self._documents: dict[str, _AsyncDocumentsResource] = {}
        self._sources: dict[str, _AsyncSourcesResource] = {}

    @property
    def projects(self) -> _AsyncProjectsResource:
        """CRUD for KB projects."""
        if self._projects is None:
            self._projects = _AsyncProjectsResource(self._config)
        return self._projects

    def documents(self, project_slug: str) -> _AsyncDocumentsResource:
        """Documents resource scoped to a project."""
        if project_slug not in self._documents:
            self._documents[project_slug] = _AsyncDocumentsResource(self._config, project_slug)
        return self._documents[project_slug]

    def sources(self, project_slug: str) -> _AsyncSourcesResource:
        """Data sources resource scoped to a project."""
        if project_slug not in self._sources:
            self._sources[project_slug] = _AsyncSourcesResource(self._config, project_slug)
        return self._sources[project_slug]

    @async_api_error_handler
    async def search(
        self,
        project_slug: str,
        query: str,
        *,
        limit: int = 10,
        threshold: float | None = None,
    ) -> SearchResponse:
        """Semantic vector search within a knowledge base project."""
        logger.debug("KB async search: project=%s query=%r limit=%d", project_slug, query, limit)
        data = SearchRequestRequest(query=query, limit=limit, threshold=threshold)
        result = await self._api.projects_search_create(project_slug=project_slug, data=data)
        logger.info("KB async search: project=%s found=%d", project_slug, result.total)
        return result


class _AsyncProjectsResource(AsyncBaseResource):
    """CRUD for Knowledge Base projects (async)."""

    def __init__(self, config: SDKConfig) -> None:
        super().__init__(config)
        self._api = _AsyncAPI(self._http_client)

    @async_api_error_handler
    async def list(self, *, page: int = 1, page_size: int = 20) -> PaginatedProjectList:
        """List your KB projects."""
        return await self._api.projects_list(page=page, page_size=page_size)  # type: ignore[return-value]

    @async_api_error_handler
    async def create(
        self,
        *,
        name: str,
        slug: str,
        description: str | None = None,
        is_public: bool = False,
        embedding_model: str | None = None,
        similarity_threshold: float | None = None,
        max_search_results: int | None = None,
    ) -> ProjectCreate:
        """Create a new KB project."""
        data = ProjectCreateRequest(
            name=name,
            slug=slug,
            description=description,
            is_public=is_public,
            embedding_model=embedding_model,
            similarity_threshold=similarity_threshold,
            max_search_results=max_search_results,
        )
        return await self._api.projects_create(data=data)

    @async_api_error_handler
    async def get(self, slug: str) -> Project:
        """Get a project by slug."""
        return await self._api.projects_retrieve(slug=slug)

    @async_api_error_handler
    async def update(
        self,
        slug: str,
        *,
        name: str | None = None,
        description: str | None = None,
        is_public: bool | None = None,
        similarity_threshold: float | None = None,
        max_search_results: int | None = None,
    ) -> Project:
        """Partially update a project."""
        data = PatchedProjectRequest(  # type: ignore[call-arg]
            name=name,
            description=description,
            is_public=is_public,
            similarity_threshold=similarity_threshold,
            max_search_results=max_search_results,
        )
        return await self._api.projects_partial_update(slug=slug, data=data)

    @async_api_error_handler
    async def delete(self, slug: str) -> None:
        """Delete a project and all its documents, chunks, and sources."""
        await self._api.projects_destroy(slug=slug)


class _AsyncDocumentsResource(AsyncBaseResource):
    """Documents within a KB project (async)."""

    def __init__(self, config: SDKConfig, project_slug: str) -> None:
        super().__init__(config, use_upload_timeout=True)
        self._api = _AsyncAPI(self._http_client)
        self._slug = project_slug

    @async_api_error_handler
    async def list(self, *, page: int = 1, page_size: int = 20) -> PaginatedDocumentList:
        """List documents in this project."""
        return await self._api.projects_documents_list(  # type: ignore[return-value]
            project_slug=self._slug, page=page, page_size=page_size,
        )

    @async_api_error_handler
    async def upload(
        self,
        *,
        title: str,
        content: str,
        source_url: str | None = None,
    ) -> Document:
        """Upload a document (Markdown/plain text)."""
        data = DocumentUploadRequest(title=title, content=content, source_url=source_url)
        return await self._api.projects_documents_create(project_slug=self._slug, data=data)

    @async_api_error_handler
    async def upload_file(self, path: Path, *, source_url: str | None = None) -> Document:
        """Upload a local Markdown or text file."""
        path = Path(path)
        content = path.read_text(encoding="utf-8")
        title = path.stem.replace("-", " ").replace("_", " ").title()
        return await self.upload(title=title, content=content, source_url=source_url)

    @async_api_error_handler
    async def get(self, document_id: str) -> Document:
        """Get a document by ID."""
        return await self._api.projects_documents_retrieve(id=document_id, project_slug=self._slug)

    @async_api_error_handler
    async def delete(self, document_id: str) -> None:
        """Delete a document and all its chunks."""
        await self._api.projects_documents_destroy(id=document_id, project_slug=self._slug)


class _AsyncSourcesResource(AsyncBaseResource):
    """GitHub data sources for a KB project (async)."""

    def __init__(self, config: SDKConfig, project_slug: str) -> None:
        super().__init__(config)
        self._api = _AsyncAPI(self._http_client)
        self._slug = project_slug

    @async_api_error_handler
    async def list(self, *, page: int = 1, page_size: int = 20) -> PaginatedDataSourceList:
        """List data sources for this project."""
        return await self._api.projects_sources_list(  # type: ignore[return-value]
            project_slug=self._slug, page=page, page_size=page_size,
        )

    @async_api_error_handler
    async def add(
        self,
        *,
        url: str,
        branch: str = "main",
        path_filter: str | None = None,
        crawl_interval_hours: int = 24,
    ) -> DataSource:
        """Add a GitHub repository as a data source."""
        data = DataSourceCreateRequest(
            source_type=DataSourceSourceType.GITHUB_REPO,
            url=url,
            branch=branch,
            path_filter=path_filter,
            crawl_interval_hours=crawl_interval_hours,
        )
        return await self._api.projects_sources_create(project_slug=self._slug, data=data)  # type: ignore[return-value]

    @async_api_error_handler
    async def get(self, source_id: str) -> DataSource:
        """Get a source by ID."""
        return await self._api.projects_sources_retrieve(id=source_id, project_slug=self._slug)

    @async_api_error_handler
    async def update(
        self,
        source_id: str,
        *,
        branch: str | None = None,
        path_filter: str | None = None,
        crawl_interval_hours: int | None = None,
        is_active: bool | None = None,
    ) -> DataSource:
        """Update a data source."""
        data = PatchedDataSourceRequest(  # type: ignore[call-arg]
            branch=branch,
            path_filter=path_filter,
            crawl_interval_hours=crawl_interval_hours,
            is_active=is_active,
        )
        return await self._api.projects_sources_partial_update(
            id=source_id, project_slug=self._slug, data=data,
        )

    @async_api_error_handler
    async def delete(self, source_id: str) -> None:
        """Remove a data source."""
        await self._api.projects_sources_destroy(id=source_id, project_slug=self._slug)

    @async_api_error_handler
    async def crawl(self, source_id: str) -> None:
        """Trigger an immediate crawl of this data source (background RQ job)."""
        source = await self.get(source_id)
        crawl_data = DataSourceRequest(
            source_type=source.source_type,
            url=source.url,
            branch=source.branch,
            path_filter=source.path_filter,
            crawl_interval_hours=source.crawl_interval_hours,
            is_active=source.is_active,
        )
        await self._api.projects_sources_crawl_create(
            id=source_id, project_slug=self._slug, data=crawl_data,
        )


__all__ = [
    "KnowledgeBaseResource",
    "AsyncKnowledgeBaseResource",
    # Models
    "Project",
    "ProjectCreate",
    "PaginatedProjectList",
    "Document",
    "PaginatedDocumentList",
    "DataSource",
    "PaginatedDataSourceList",
    "SearchResponse",
    "SearchRequestRequest",
]
