"""
PatentsView PatentSearch API Client

Note: The PatentsView API (search.patentsview.org) was shut down on March 20, 2026.
This client is retained for reference but is no longer functional. PatentsView data
has been migrated to the USPTO Open Data Portal as bulk downloadable datasets.

Original API Reference: https://search.patentsview.org/docs/docs/Search%20API/SearchAPIReference/
"""

import asyncio
import json
import logging
import time
from typing import Any, Optional, Dict, List
import httpx
from tenacity import (
    retry,
    stop_after_attempt,
    wait_exponential,
    retry_if_exception_type,
)

from patent_mcp_server.util.logging import LoggingTransport
from patent_mcp_server.util.errors import ApiError
from patent_mcp_server.config import config
from patent_mcp_server.constants import HTTPMethods, Defaults, PatentsViewEndpoints

logger = logging.getLogger('patentsview_client')


class PatentsViewClient:
    """Client for the PatentsView PatentSearch API.

    Provides access to USPTO patent data through PatentsView including:
    - Full patent search with advanced query syntax
    - Inventor and assignee disambiguation
    - CPC classification lookups
    - Patent claims and description text
    - Foreign citations

    Rate limit: 45 requests per minute
    """

    def __init__(self):
        self.base_url = config.PATENTSVIEW_BASE_URL
        self.api_key = config.PATENTSVIEW_API_KEY
        self.rate_limit = config.PATENTSVIEW_RATE_LIMIT  # requests per minute

        # Rate limiting state
        self._request_times: List[float] = []
        self._rate_limit_lock = asyncio.Lock()

        self.headers = {
            "User-Agent": config.USER_AGENT,
            "Accept": "application/json",
            "Content-Type": "application/json",
        }

        if self.api_key:
            self.headers["X-Api-Key"] = self.api_key

        transport = httpx.AsyncHTTPTransport()
        logging_transport = LoggingTransport(transport)

        self.client = httpx.AsyncClient(
            headers=self.headers,
            http2=True,
            follow_redirects=True,
            transport=logging_transport,
            timeout=config.REQUEST_TIMEOUT,
        )

    async def __aenter__(self):
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        await self.close()

    async def _check_rate_limit(self):
        """Check and enforce rate limiting (45 requests/minute)."""
        async with self._rate_limit_lock:
            now = time.time()
            # Remove requests older than 60 seconds
            self._request_times = [t for t in self._request_times if now - t < 60]

            if len(self._request_times) >= self.rate_limit:
                # Wait until oldest request expires
                wait_time = 60 - (now - self._request_times[0])
                if wait_time > 0:
                    logger.warning(f"Rate limit reached, waiting {wait_time:.1f}s")
                    await asyncio.sleep(wait_time)
                    self._request_times = []

            self._request_times.append(time.time())

    @retry(
        stop=stop_after_attempt(config.MAX_RETRIES),
        wait=wait_exponential(
            multiplier=config.RETRY_DELAY,
            min=config.RETRY_MIN_WAIT,
            max=config.RETRY_MAX_WAIT
        ),
        retry=retry_if_exception_type((httpx.TimeoutException, httpx.NetworkError)),
        reraise=True
    )
    async def _make_request(
        self,
        endpoint: str,
        method: str = HTTPMethods.GET,
        params: Optional[Dict[str, Any]] = None,
        data: Optional[Dict[str, Any]] = None
    ) -> Dict[str, Any]:
        """Make a request to the PatentsView API.

        Args:
            endpoint: API endpoint path
            method: HTTP method (GET or POST)
            params: Query parameters
            data: JSON body for POST requests

        Returns:
            Response JSON dictionary or error dictionary
        """
        await self._check_rate_limit()

        url = f"{self.base_url}{endpoint}"
        logger.info(f"Making {method} request to {url}")

        try:
            if method == HTTPMethods.GET:
                response = await self.client.get(url, params=params)
            else:
                response = await self.client.post(url, json=data)

            # Handle rate limit response
            if response.status_code == 429:
                retry_after = int(response.headers.get("Retry-After", 60))
                logger.warning(f"Rate limited, waiting {retry_after}s")
                await asyncio.sleep(retry_after)
                # Retry the request
                if method == HTTPMethods.GET:
                    response = await self.client.get(url, params=params)
                else:
                    response = await self.client.post(url, json=data)

            response.raise_for_status()
            data = response.json()

            # PatentsView can return errors with HTTP 200 - normalize them
            if isinstance(data.get("error"), bool) and data.get("error") is True:
                message = data.get("message")
                if not isinstance(message, str) or not message:
                    message = "PatentsView API error"
                return ApiError.create(
                    message=message,
                    status_code=data.get("status_code", 200),
                    details={"raw_response": data}
                )

            return data

        except httpx.HTTPStatusError as e:
            status_code = e.response.status_code
            status_reason = e.response.headers.get("X-Status-Reason", "")
            logger.error(
                f"HTTP {status_code} error for {method} {url}: {status_reason}. "
                f"Request params={params}, body={data}"
            )

            try:
                error_json = e.response.json()
                # PatentsView uses X-Status-Reason header for actual error details
                error_message = status_reason if status_reason else e.response.text
                return ApiError.from_http_error(
                    status_code=status_code,
                    response_text=error_message,
                    response_json=error_json
                )
            except Exception:
                error_message = status_reason if status_reason else e.response.text
                return ApiError.from_http_error(
                    status_code=status_code,
                    response_text=error_message
                )

        except (httpx.TimeoutException, httpx.NetworkError) as e:
            logger.warning(f"Network error (will retry): {str(e)}")
            raise

        except Exception as e:
            logger.error(f"Unexpected error: {str(e)}")
            return ApiError.from_exception(e, "PatentsView API request failed")

    def _build_query(
        self,
        q: Dict[str, Any],
        f: Optional[List[str]] = None,
        s: Optional[List[Dict[str, str]]] = None,
        o: Optional[Dict[str, Any]] = None,
        for_post: bool = True,
    ) -> Dict[str, Any]:
        """Build a PatentsView API query object.

        Args:
            q: Query criteria (required)
            f: Fields to return
            s: Sort order
            o: Options (pagination, etc.)
            for_post: If True, return raw dicts for POST body.
                      If False, return JSON-stringified values for GET params.

        Returns:
            Query dict for POST body (for_post=True) or
            Query parameters dict with JSON strings (for_post=False)
        """
        if for_post:
            # For POST requests: return raw objects as JSON body
            body = {"q": q}
            if f:
                body["f"] = f
            if s:
                body["s"] = s
            if o:
                body["o"] = o
            return body
        else:
            # For GET requests: JSON-stringify values for URL params
            params = {"q": json.dumps(q)}
            if f:
                params["f"] = json.dumps(f)
            if s:
                params["s"] = json.dumps(s)
            if o:
                params["o"] = json.dumps(o)
            return params

    async def search_patents(
        self,
        query: Dict[str, Any],
        fields: Optional[List[str]] = None,
        sort: Optional[List[Dict[str, str]]] = None,
        size: int = 100,
        after: Optional[str] = None,
        exclude_withdrawn: bool = True,
    ) -> Dict[str, Any]:
        """Search patents using PatentsView query syntax.

        Args:
            query: Query object using PatentsView syntax
                   Example: {"patent_title": "neural network"}
                   Example: {"_and": [{"patent_date": {"_gte": "2020-01-01"}}, {"assignee_organization": "IBM"}]}
            fields: List of fields to return
            sort: Sort order list, e.g., [{"patent_date": "desc"}]
            size: Results per page (max 1000, default 100)
            after: Cursor for pagination from previous response
            exclude_withdrawn: Exclude withdrawn patents (default True)

        Returns:
            Dictionary containing search results
        """
        options = {"size": min(size, 1000)}
        if after:
            options["after"] = after
        if not exclude_withdrawn:
            options["exclude_withdrawn"] = False

        body = self._build_query(query, fields, sort, options, for_post=True)

        return await self._make_request(
            PatentsViewEndpoints.PATENT,
            method=HTTPMethods.POST,
            data=body
        )

    async def get_patent(self, patent_id: str) -> Dict[str, Any]:
        """Get a specific patent by ID.

        Args:
            patent_id: Patent ID (e.g., "7861317")

        Returns:
            Dictionary containing patent details
        """
        return await self._make_request(f"{PatentsViewEndpoints.PATENT}{patent_id}/")

    async def search_by_text(
        self,
        text: str,
        search_type: str = "any",
        fields: Optional[List[str]] = None,
        size: int = 100,
    ) -> Dict[str, Any]:
        """Search patents by full-text in title and abstract.

        Args:
            text: Text to search for
            search_type: "any" (match any word), "all" (match all words), or "phrase" (exact phrase)
            fields: List of fields to return
            size: Results per page (max 1000)

        Returns:
            Dictionary containing search results
        """
        if search_type == "phrase":
            query = {"_or": [
                {"_text_phrase": {"patent_title": text}},
                {"_text_phrase": {"patent_abstract": text}},
            ]}
        elif search_type == "all":
            query = {"_or": [
                {"_text_all": {"patent_title": text}},
                {"_text_all": {"patent_abstract": text}},
            ]}
        else:  # any
            query = {"_or": [
                {"_text_any": {"patent_title": text}},
                {"_text_any": {"patent_abstract": text}},
            ]}

        default_fields = fields or [
            "patent_id", "patent_title", "patent_abstract", "patent_date",
            "patent_type",
            "assignees.assignee_organization", "assignees.assignee_type",
            "inventors.inventor_name_first", "inventors.inventor_name_last",
            "cpc_current.cpc_group_id", "cpc_current.cpc_subclass_id",
        ]

        return await self.search_patents(query, fields=default_fields, size=size)

    async def search_assignees(
        self,
        query: Dict[str, Any],
        fields: Optional[List[str]] = None,
        size: int = 100,
    ) -> Dict[str, Any]:
        """Search disambiguated assignees.

        Args:
            query: Query object for assignee search
                   Example: {"_text_any": {"assignee_organization": "IBM"}}
            fields: List of fields to return
            size: Results per page

        Returns:
            Dictionary containing assignee search results
        """
        options = {"size": min(size, 1000)}
        body = self._build_query(query, fields, o=options, for_post=True)

        return await self._make_request(
            PatentsViewEndpoints.ASSIGNEE,
            method=HTTPMethods.POST,
            data=body
        )

    async def get_assignee(self, assignee_id: str) -> Dict[str, Any]:
        """Get a specific assignee by ID.

        Args:
            assignee_id: Disambiguated assignee ID

        Returns:
            Dictionary containing assignee details
        """
        return await self._make_request(f"{PatentsViewEndpoints.ASSIGNEE}{assignee_id}/")

    async def search_inventors(
        self,
        query: Dict[str, Any],
        fields: Optional[List[str]] = None,
        size: int = 100,
    ) -> Dict[str, Any]:
        """Search disambiguated inventors.

        Args:
            query: Query object for inventor search
                   Example: {"_text_any": {"inventor_name_last": "Smith"}}
            fields: List of fields to return
            size: Results per page

        Returns:
            Dictionary containing inventor search results
        """
        options = {"size": min(size, 1000)}
        body = self._build_query(query, fields, o=options, for_post=True)

        return await self._make_request(
            PatentsViewEndpoints.INVENTOR,
            method=HTTPMethods.POST,
            data=body
        )

    async def get_inventor(self, inventor_id: str) -> Dict[str, Any]:
        """Get a specific inventor by ID.

        Args:
            inventor_id: Disambiguated inventor ID

        Returns:
            Dictionary containing inventor details
        """
        return await self._make_request(f"{PatentsViewEndpoints.INVENTOR}{inventor_id}/")

    async def get_patent_claims(
        self,
        patent_id: str,
        fields: Optional[List[str]] = None,
    ) -> Dict[str, Any]:
        """Get claims for a specific patent.

        Args:
            patent_id: Patent ID
            fields: List of fields to return

        Returns:
            Dictionary containing patent claims
        """
        query = {"patent_id": patent_id}
        default_fields = fields or ["patent_id", "claim_sequence", "claim_text"]
        body = self._build_query(query, default_fields, for_post=True)

        return await self._make_request(
            PatentsViewEndpoints.CLAIMS,
            method=HTTPMethods.POST,
            data=body
        )

    async def get_patent_summary(
        self,
        patent_id: str,
    ) -> Dict[str, Any]:
        """Get brief summary text for a specific patent.

        Args:
            patent_id: Patent ID

        Returns:
            Dictionary containing patent brief summary
        """
        query = {"patent_id": patent_id}
        body = self._build_query(query, for_post=True)

        return await self._make_request(
            PatentsViewEndpoints.BRIEF_SUMMARY,
            method=HTTPMethods.POST,
            data=body
        )

    async def get_patent_description(
        self,
        patent_id: str,
    ) -> Dict[str, Any]:
        """Get detailed description text for a specific patent.

        Args:
            patent_id: Patent ID

        Returns:
            Dictionary containing patent detailed description
        """
        query = {"patent_id": patent_id}
        body = self._build_query(query, for_post=True)

        return await self._make_request(
            PatentsViewEndpoints.DESCRIPTION,
            method=HTTPMethods.POST,
            data=body
        )

    async def search_by_cpc(
        self,
        cpc_code: str,
        fields: Optional[List[str]] = None,
        size: int = 100,
    ) -> Dict[str, Any]:
        """Search patents by CPC classification code.

        Args:
            cpc_code: CPC code (e.g., "G06N3/08")
            fields: List of fields to return
            size: Results per page

        Returns:
            Dictionary containing patent search results
        """
        # Handle both exact match and prefix match
        if "/" in cpc_code:
            query = {"cpc_current.cpc_group_id": cpc_code}
        else:
            query = {"cpc_current.cpc_subclass_id": {"_begins": cpc_code}}

        default_fields = fields or [
            "patent_id", "patent_title", "patent_date",
            "cpc_current.cpc_group_id", "cpc_current.cpc_subclass_id",
            "assignees.assignee_organization",
        ]

        return await self.search_patents(query, fields=default_fields, size=size)

    async def lookup_cpc_class(self, cpc_class: str) -> Dict[str, Any]:
        """Look up CPC class information.

        Args:
            cpc_class: CPC class code (e.g., "G06")

        Returns:
            Dictionary containing CPC class details
        """
        return await self._make_request(f"{PatentsViewEndpoints.CPC_CLASS}{cpc_class}/")

    async def lookup_cpc_group(self, cpc_group: str) -> Dict[str, Any]:
        """Look up CPC group information.

        Args:
            cpc_group: CPC group code (e.g., "G06N3/08")

        Returns:
            Dictionary containing CPC group details
        """
        # Convert slash to colon for API URL (e.g., "G06N3/08" → "G06N3:08")
        cpc_group_formatted = cpc_group.replace("/", ":")
        return await self._make_request(f"{PatentsViewEndpoints.CPC_GROUP}{cpc_group_formatted}/")

    async def search_publications(
        self,
        query: Dict[str, Any],
        fields: Optional[List[str]] = None,
        size: int = 100,
    ) -> Dict[str, Any]:
        """Search pregrant publications.

        Args:
            query: Query object for publication search
            fields: List of fields to return
            size: Results per page

        Returns:
            Dictionary containing publication search results
        """
        options = {"size": min(size, 1000)}
        body = self._build_query(query, fields, o=options, for_post=True)

        return await self._make_request(
            PatentsViewEndpoints.PUBLICATION,
            method=HTTPMethods.POST,
            data=body
        )

    async def get_foreign_citations(
        self,
        patent_id: str,
        fields: Optional[List[str]] = None,
    ) -> Dict[str, Any]:
        """Get foreign citations for a specific patent.

        Args:
            patent_id: Patent ID
            fields: List of fields to return

        Returns:
            Dictionary containing foreign citations
        """
        query = {"patent_id": patent_id}
        body = self._build_query(query, fields, for_post=True)

        return await self._make_request(
            PatentsViewEndpoints.FOREIGN_CITATION,
            method=HTTPMethods.POST,
            data=body
        )

    async def search_attorneys(
        self,
        query: Dict[str, Any],
        fields: Optional[List[str]] = None,
        size: int = 100,
    ) -> Dict[str, Any]:
        """Search patent attorneys.

        Args:
            query: Query object for attorney search
                   Example: {"_text_any": {"attorney_name_last": "Smith"}}
                   Example: {"_text_any": {"attorney_organization": "LLP"}}
            fields: List of fields to return
            size: Results per page

        Returns:
            Dictionary containing attorney search results
        """
        options = {"size": min(size, 1000)}
        body = self._build_query(query, fields, o=options, for_post=True)

        return await self._make_request(
            PatentsViewEndpoints.ATTORNEY,
            method=HTTPMethods.POST,
            data=body
        )

    async def get_attorney(self, attorney_id: str) -> Dict[str, Any]:
        """Get a specific attorney by ID.

        Args:
            attorney_id: Attorney ID

        Returns:
            Dictionary containing attorney details
        """
        return await self._make_request(f"{PatentsViewEndpoints.ATTORNEY}{attorney_id}/")

    async def search_ipc(
        self,
        query: Dict[str, Any],
        fields: Optional[List[str]] = None,
        size: int = 100,
    ) -> Dict[str, Any]:
        """Search IPC (International Patent Classification) codes.

        Args:
            query: Query object for IPC search
                   Example: {"ipc_class": "G06"}
                   Example: {"ipc_subclass": {"_begins": "G06F"}}
            fields: List of fields to return
            size: Results per page

        Returns:
            Dictionary containing IPC search results
        """
        options = {"size": min(size, 1000)}
        body = self._build_query(query, fields, o=options, for_post=True)

        return await self._make_request(
            PatentsViewEndpoints.IPC,
            method=HTTPMethods.POST,
            data=body
        )

    async def lookup_ipc(self, ipc_code: str) -> Dict[str, Any]:
        """Look up IPC classification information.

        Args:
            ipc_code: IPC code (e.g., "G06F")

        Returns:
            Dictionary containing IPC classification details
        """
        return await self._make_request(f"{PatentsViewEndpoints.IPC}{ipc_code}/")

    async def close(self):
        """Close the client connections."""
        logger.info("Closing PatentsView client connections")
        await self.client.aclose()
