# src/ihyee/client.py
"""Synchronous ihyee API client."""
from __future__ import annotations

import os
from typing import Any, Dict, List, Optional

import httpx

from ihyee.exceptions import (
    AuthError,
    BadRequestError,
    IhyeeError,
    RateLimitError,
    ServerError,
)
from ihyee.models import FetchResponse, SearchResponse


def _resolve_api_key(api_key: Optional[str]) -> str:
    if api_key:
        return api_key
    key = os.environ.get("IHYEE_API_KEY")
    if key:
        return key
    raise AuthError(
        code="missing_key",
        message="No API key provided. Pass api_key= or set IHYEE_API_KEY environment variable.",
        status_code=401,
    )


def _build_payload(required: Dict[str, Any], **kwargs: Any) -> Dict[str, Any]:
    payload = dict(required)
    for key, value in kwargs.items():
        if value is not None:
            payload[key] = value
    return payload


def _handle_errors(response: httpx.Response) -> None:
    if response.is_success:
        return

    try:
        body = response.json()
        error = body.get("error", {})
        code = error.get("code", "unknown")
        message = error.get("message", response.text)
    except Exception:
        code = "unknown"
        message = response.text
        error = {}

    if response.status_code == 429:
        retry_after = error.get("retry_after_seconds")
        raise RateLimitError(code, message, retry_after)
    elif response.status_code in (401, 403):
        raise AuthError(code, message, response.status_code)
    elif response.status_code == 400:
        raise BadRequestError(code, message, response.status_code)
    else:
        raise ServerError(code, message, response.status_code)


class Ihyee:
    """Synchronous client for the ihyee web intelligence API.

    Usage::

        from ihyee import Ihyee

        client = Ihyee(api_key="your_key")
        results = client.search("transformer architecture")
        print(results.content[0].text.summary)
    """

    def __init__(
        self,
        api_key: Optional[str] = None,
        base_url: str = "https://ihyee.delta-telematics.ca",
        timeout: float = 60.0,
    ) -> None:
        resolved_key = _resolve_api_key(api_key)
        self._http = httpx.Client(
            base_url=base_url,
            headers={"Authorization": f"Bearer {resolved_key}"},
            timeout=timeout,
        )

    def search(
        self,
        query: str,
        *,
        max_results: int = 5,
        content_mode: str = "both",
        format: str = "text",
        render: bool = False,
        summarize_sentences: int = 10,
        before: Optional[str] = None,
        after: Optional[str] = None,
        date_restrict: Optional[str] = None,
        must_have: Optional[str] = None,
        intext: Optional[str] = None,
        allintext: Optional[str] = None,
        and_condition: Optional[str] = None,
        # RAG parameters (used when content_mode="rag")
        max_tokens_per_chunk: Optional[int] = None,
        max_tokens_per_result: Optional[int] = None,
        max_total_tokens: Optional[int] = None,
        min_relevance_score: Optional[float] = None,
        top_k_chunks: Optional[int] = None,
        tokenizer: Optional[str] = None,
    ) -> SearchResponse:
        """Search the web and return extracted content from top results.

        Args:
            query: Search query string.
            max_results: Number of results to fetch (1-10).
            content_mode: Output mode — "both", "full_text", "summary", "markdown", or "rag".
            format: Output format — "text" or "markdown".
            render: Force Playwright rendering for all pages.
            summarize_sentences: Max sentences in summary (1-50).
            before: Results before date (YYYY-MM-DD).
            after: Results after date (YYYY-MM-DD).
            date_restrict: Time filter (d[N], w[N], m[N], y[N]).
            must_have: Required phrase (exact match).
            intext: Required keyword in page body.
            allintext: Required words that must ALL appear in page body.
            and_condition: Additional required terms (AND logic).
            max_tokens_per_chunk: Max tokens per RAG chunk (50-2000, default 500).
            max_tokens_per_result: Token budget per result (100-10000, default 2000).
            max_total_tokens: Global token budget (500-50000, default 8000).
            min_relevance_score: Min BM25 score to include (0.0-1.0, default 0.0).
            top_k_chunks: Keep only top-K chunks per result (1-200).
            tokenizer: Token counter — "cl100k", "o200k", or "approximate".
        """
        payload = _build_payload(
            {"query": query, "max_results": max_results, "content_mode": content_mode,
             "format": format, "render": render, "summarize_sentences": summarize_sentences},
            before=before, after=after, date_restrict=date_restrict,
            must_have=must_have, intext=intext, allintext=allintext,
            and_condition=and_condition,
            max_tokens_per_chunk=max_tokens_per_chunk,
            max_tokens_per_result=max_tokens_per_result,
            max_total_tokens=max_total_tokens,
            min_relevance_score=min_relevance_score,
            top_k_chunks=top_k_chunks,
            tokenizer=tokenizer,
        )
        response = self._http.post("/v1/search", json=payload)
        _handle_errors(response)
        return SearchResponse.model_validate(response.json())

    def fetch(
        self,
        urls: List[str],
        *,
        content_mode: str = "both",
        format: str = "text",
        render: bool = False,
        summarize_sentences: int = 10,
        # RAG parameters
        max_tokens_per_chunk: Optional[int] = None,
        max_tokens_per_result: Optional[int] = None,
        max_total_tokens: Optional[int] = None,
        min_relevance_score: Optional[float] = None,
        top_k_chunks: Optional[int] = None,
        tokenizer: Optional[str] = None,
    ) -> FetchResponse:
        """Fetch and extract content from specific URLs.

        Args:
            urls: List of URLs to fetch (1-10).
            content_mode: Output mode — "both", "full_text", "summary", "markdown", or "rag".
            format: Output format — "text" or "markdown".
            render: Force Playwright rendering for all pages.
            summarize_sentences: Max sentences in summary (1-50).
            max_tokens_per_chunk: Max tokens per RAG chunk (50-2000, default 500).
            max_tokens_per_result: Token budget per result (100-10000, default 2000).
            max_total_tokens: Global token budget (500-50000, default 8000).
            min_relevance_score: Min BM25 score to include (0.0-1.0, default 0.0).
            top_k_chunks: Keep only top-K chunks per result (1-200).
            tokenizer: Token counter — "cl100k", "o200k", or "approximate".
        """
        payload = _build_payload(
            {"urls": urls, "content_mode": content_mode, "format": format,
             "render": render, "summarize_sentences": summarize_sentences},
            max_tokens_per_chunk=max_tokens_per_chunk,
            max_tokens_per_result=max_tokens_per_result,
            max_total_tokens=max_total_tokens,
            min_relevance_score=min_relevance_score,
            top_k_chunks=top_k_chunks,
            tokenizer=tokenizer,
        )
        response = self._http.post("/v1/fetch", json=payload)
        _handle_errors(response)
        return FetchResponse.model_validate(response.json())

    def render(
        self,
        url: str,
        *,
        wait_for: str = "networkidle",
        wait_selector: Optional[str] = None,
        timeout_ms: int = 30000,
        content_mode: str = "both",
        format: str = "text",
        summarize_sentences: int = 10,
        # RAG parameters
        max_tokens_per_chunk: Optional[int] = None,
        max_tokens_per_result: Optional[int] = None,
        max_total_tokens: Optional[int] = None,
        min_relevance_score: Optional[float] = None,
        top_k_chunks: Optional[int] = None,
        tokenizer: Optional[str] = None,
    ) -> FetchResponse:
        """Force full Playwright render of a URL.

        Args:
            url: URL to render with headless browser.
            wait_for: Wait strategy — "networkidle", "domcontentloaded", or "load".
            wait_selector: CSS selector to wait for after navigation.
            timeout_ms: Navigation timeout in ms (1000-60000).
            content_mode: Output mode — "both", "full_text", "summary", "markdown", or "rag".
            format: Output format — "text" or "markdown".
            summarize_sentences: Max sentences in summary (1-50).
            max_tokens_per_chunk: Max tokens per RAG chunk (50-2000, default 500).
            max_tokens_per_result: Token budget per result (100-10000, default 2000).
            max_total_tokens: Global token budget (500-50000, default 8000).
            min_relevance_score: Min BM25 score to include (0.0-1.0, default 0.0).
            top_k_chunks: Keep only top-K chunks per result (1-200).
            tokenizer: Token counter — "cl100k", "o200k", or "approximate".
        """
        payload = _build_payload(
            {"url": url, "wait_for": wait_for, "timeout_ms": timeout_ms,
             "content_mode": content_mode, "format": format,
             "summarize_sentences": summarize_sentences},
            wait_selector=wait_selector,
            max_tokens_per_chunk=max_tokens_per_chunk,
            max_tokens_per_result=max_tokens_per_result,
            max_total_tokens=max_total_tokens,
            min_relevance_score=min_relevance_score,
            top_k_chunks=top_k_chunks,
            tokenizer=tokenizer,
        )
        response = self._http.post("/v1/render", json=payload)
        _handle_errors(response)
        return FetchResponse.model_validate(response.json())

    def close(self) -> None:
        """Close the HTTP client."""
        self._http.close()

    def __enter__(self) -> Ihyee:
        return self

    def __exit__(self, *args: Any) -> None:
        self.close()
