"""Daemon client for mykb."""

import logging
import time

import httpx

from mykb.backends.base import SearchHit, SearchResult
from mykb.daemon.process import (
    get_daemon_url,
    is_daemon_running,
    start_daemon,
    wait_for_daemon_ready,
)

logger = logging.getLogger(__name__)


class DaemonClientError(Exception):
    """Error communicating with daemon."""

    pass


class DaemonClient:
    """
    HTTP client for mykb daemon.

    Provides methods to call daemon API endpoints for embedding and search operations.
    Can automatically start the daemon if not running.
    """

    def __init__(self, base_url: str | None = None, timeout: float = 60.0, auto_start: bool = True):
        """
        Initialize daemon client.

        Args:
            base_url: Daemon URL. If None, uses config default.
            timeout: Request timeout in seconds.
            auto_start: If True, automatically start daemon if not running.
        """
        self.base_url = base_url or get_daemon_url()
        self.timeout = timeout
        self.auto_start = auto_start
        self._client: httpx.Client | None = None

    def _get_client(self) -> httpx.Client:
        """Get or create HTTP client."""
        if self._client is None:
            self._client = httpx.Client(base_url=self.base_url, timeout=self.timeout)
        return self._client

    def close(self):
        """Close the HTTP client."""
        if self._client is not None:
            self._client.close()
            self._client = None

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.close()

    def _is_daemon_running(self) -> bool:
        """Check if daemon is running and healthy."""
        running, _ = is_daemon_running()
        return running

    def _ensure_daemon(self):
        """Ensure daemon is running, starting it if needed."""
        if self._is_daemon_running():
            return

        if not self.auto_start:
            raise DaemonClientError('Daemon is not running and auto_start is disabled')

        logger.info('Daemon not running, starting...')
        success = start_daemon(foreground=False)
        if not success:
            raise DaemonClientError('Failed to start daemon')

        # Wait for daemon to be ready
        if not wait_for_daemon_ready(timeout=60.0):
            raise DaemonClientError('Daemon failed to become ready')

        logger.info('Daemon started successfully')

    def _request(self, method: str, path: str, **kwargs) -> httpx.Response:
        """Make HTTP request with error handling."""
        self._ensure_daemon()
        client = self._get_client()
        resp = client.request(method, path, **kwargs)
        resp.raise_for_status()
        return resp

    def health(self) -> dict:
        """
        Get daemon health status.

        Returns:
            Health info dict with status, model_loaded, model_name, backend, uptime.
        """
        resp = self._request('GET', '/health')
        return resp.json()

    def encode_documents(self, texts: list[str]) -> list[list[float]]:
        """
        Batch encode document texts into embedding vectors.

        Args:
            texts: List of document texts to encode.

        Returns:
            List of embedding vectors.
        """
        resp = self._request('POST', '/embed/documents', json={'texts': texts})
        return resp.json()['vectors']

    def encode_query(self, text: str) -> list[float]:
        """
        Encode a search query into an embedding vector.

        Args:
            text: Query text to encode.

        Returns:
            Embedding vector.
        """
        resp = self._request('POST', '/embed/query', json={'text': text})
        return resp.json()['vector']

    def index_collection(
        self,
        collection: str,
        full: bool = False,
        with_embeddings: bool = True,
    ) -> dict:
        """
        Index a collection.

        Args:
            collection: Collection name.
            full: If True, force full re-index.
            with_embeddings: If True, generate embeddings during indexing.

        Returns:
            Result dict with collection, chunks, elapsed.
        """
        resp = self._request(
            'POST',
            '/collection/index',
            json={
                'collection': collection,
                'full': full,
                'with_embeddings': with_embeddings,
            },
        )
        return resp.json()

    def embed_collection(self, collection: str, full: bool = False) -> dict:
        """
        Generate/update embeddings for an indexed collection.

        Args:
            collection: Collection name.
            full: If True, force re-embed all documents.

        Returns:
            Result dict with collection, updated.
        """
        resp = self._request(
            'POST',
            '/collection/embed',
            json={'collection': collection, 'full': full},
        )
        return resp.json()

    def vector_search(
        self,
        query: str,
        limit: int = 10,
        collection: str | None = None,
    ) -> SearchResult:
        """
        Vector semantic search.

        Args:
            query: Search query text.
            limit: Maximum number of results.
            collection: Filter by collection name.

        Returns:
            SearchResult with hits and processing time.
        """
        t0 = time.perf_counter()
        resp = self._request(
            'POST',
            '/search/vector',
            json={
                'query': query,
                'limit': limit,
                'collection': collection,
            },
        )
        data = resp.json()
        elapsed = time.perf_counter() - t0
        logger.debug(f'Vector search via daemon ({elapsed:.3f}s)')

        hits = [SearchHit(id=h['id'], score=h['score'], document=h['document']) for h in data['hits']]
        return SearchResult(
            hits=hits,
            processing_time_ms=data['processing_time_ms'],
        )

    def hybrid_search(
        self,
        query: str,
        limit: int = 10,
        collection: str | None = None,
        semantic_ratio: float = 0.5,
    ) -> SearchResult:
        """
        Hybrid search (BM25 + vector).

        Args:
            query: Search query text.
            limit: Maximum number of results.
            collection: Filter by collection name.
            semantic_ratio: Balance between BM25 (0) and vector (1).

        Returns:
            SearchResult with hits and processing time.
        """
        t0 = time.perf_counter()
        resp = self._request(
            'POST',
            '/search/hybrid',
            json={
                'query': query,
                'limit': limit,
                'collection': collection,
                'semantic_ratio': semantic_ratio,
            },
        )
        data = resp.json()
        elapsed = time.perf_counter() - t0
        logger.debug(f'Hybrid search via daemon ({elapsed:.3f}s)')

        hits = [SearchHit(id=h['id'], score=h['score'], document=h['document']) for h in data['hits']]
        return SearchResult(
            hits=hits,
            processing_time_ms=data['processing_time_ms'],
        )
