from __future__ import annotations

from typing import Any, Dict, List, Optional, Union, Tuple
import os
import time
import httpx
from dataclasses import dataclass, asdict
import threading
import random
import logging

log = logging.getLogger(__name__)


@dataclass
class PDLCompanyEnrichmentRequest:
    """Request for company enrichment"""

    name: Optional[str] = None
    website: Optional[str] = None
    domain: Optional[str] = None
    ticker: Optional[str] = None
    linkedin_url: Optional[str] = None
    location: Optional[Dict[str, str]] = None
    pretty: bool = True
    min_likelihood: int = 0


@dataclass
class PDLPersonEnrichmentRequest:
    """Request for person enrichment"""

    name: Optional[List[str]] = None
    first_name: Optional[str] = None
    last_name: Optional[str] = None
    middle_name: Optional[str] = None
    location: Optional[Dict[str, str]] = None
    street_address: Optional[str] = None
    locality: Optional[str] = None
    region: Optional[str] = None
    country: Optional[str] = None
    postal_code: Optional[str] = None
    company: Optional[str] = None
    email: Optional[str] = None
    email_hash: Optional[str] = None
    phone: Optional[str] = None
    profile: Optional[str] = None
    lid: Optional[str] = None
    birth_date: Optional[str] = None
    min_likelihood: int = 0
    required: Optional[str] = None
    pretty: bool = True


class _TokenBucket:
    """Simple token bucket for rate limiting"""

    def __init__(self, capacity: int, refill_rate_per_sec: float) -> None:
        self.capacity = max(1, capacity)
        self.tokens = float(self.capacity)
        self.refill = float(refill_rate_per_sec)
        self._last = time.time()

    def wait_for_token(self) -> None:
        now = time.time()
        elapsed = max(0.0, now - self._last)
        self._last = now
        self.tokens = min(self.capacity, self.tokens + elapsed * self.refill)
        if self.tokens < 1.0:
            need = 1.0 - self.tokens
            delay = need / max(0.001, self.refill)
            time.sleep(delay)
            self.tokens = 0.0
            self._last = time.time()
        else:
            self.tokens -= 1.0


class PeopleDataLabsAdapter:
    """People Data Labs API adapter for company and person enrichment.

    PDL offers high-quality B2B data for RevOps and Marketing Ops:
    - Company enrichment with 50+ attributes including technographics
    - Person enrichment with 400+ attributes including work history
    - Bulk search capabilities
    - Intent data and buyer signals
    - Job postings data

    Configure with env:
      PDL_API_KEY
      PDL_BASE_URL (optional; default https://api.peopledatalabs.com/v5)
      PDL_RPS (optional; default 10 for free tier, 100 for paid)
    """

    def __init__(
        self,
        api_key: Optional[str] = None,
        base_url: Optional[str] = None,
        rps: Optional[int] = None,
        timeout: Optional[float] = None,
        cache_ttl_sec: Optional[int] = None,
        cache_size: Optional[int] = None,
    ) -> None:
        self.api_key = api_key or os.getenv("PDL_API_KEY", "")
        self.base_url = (
            base_url or os.getenv("PDL_BASE_URL", "https://api.peopledatalabs.com/v5")
        ).rstrip("/")
        self.rps = int(os.getenv("PDL_RPS", str(rps or 10)))
        self._bucket = _TokenBucket(
            capacity=max(1, self.rps * 2), refill_rate_per_sec=float(self.rps)
        )
        self._client = httpx.Client(timeout=timeout or 30.0)
        # Simple in-memory TTL caches to prevent duplicate charges within a window
        self._cache_ttl = int(
            os.getenv("PDL_CACHE_TTL", str(cache_ttl_sec or 3600))
        )  # default 1h
        self._cache_size = int(os.getenv("PDL_CACHE_SIZE", str(cache_size or 5000)))
        self._company_cache: Dict[str, Tuple[float, Dict[str, Any]]] = {}
        self._person_cache: Dict[str, Tuple[float, Dict[str, Any]]] = {}
        self._cache_lock = threading.Lock()

    def _headers(self) -> Dict[str, str]:
        return {
            "X-Api-Key": self.api_key,
            "Accept": "application/json",
            "Content-Type": "application/json",
        }

    def available(self) -> bool:
        return bool(self.api_key)

    # ---- HTTP helpers with retry/backoff ----
    def _request(
        self,
        method: str,
        path: str,
        *,
        params: Optional[Dict] = None,
        json: Optional[Dict] = None,
    ) -> Dict[str, Any]:
        """Perform HTTP request with token bucket, retries and backoff.

        Retries on 429 and 5xx up to 4 attempts, honoring Retry-After if present.
        """
        if not self.available():
            return {"error": "PDL API key not configured"}

        url = f"{self.base_url}{path if path.startswith('/') else '/' + path}"
        attempts = 0
        max_attempts = 4
        backoff = 0.8

        while True:
            attempts += 1
            self._bucket.wait_for_token()
            try:
                resp = self._client.request(
                    method.upper(),
                    url,
                    params=params,
                    json=json,
                    headers=self._headers(),
                )
                # Fast path
                if 200 <= resp.status_code < 300:
                    return resp.json()

                # Handle rate limit / server errors
                if (
                    resp.status_code in (429, 500, 502, 503, 504)
                    and attempts < max_attempts
                ):
                    # Honor Retry-After if present
                    retry_after = resp.headers.get("Retry-After")
                    try:
                        delay = float(retry_after) if retry_after else backoff
                    except Exception:
                        delay = backoff
                    # jitter
                    delay = delay * (1.0 + random.random() * 0.25)
                    time.sleep(min(10.0, max(0.2, delay)))
                    backoff *= 1.8
                    continue

                # Non-retriable error
                try:
                    data = resp.json()
                except Exception:
                    data = {"error": resp.text}
                data.setdefault("status", resp.status_code)
                return data
            except httpx.HTTPError as e:
                if attempts < max_attempts:
                    time.sleep(min(5.0, backoff))
                    backoff *= 1.8
                    continue
                return {"error": str(e)}

    def enrich_company(
        self,
        name: Optional[str] = None,
        website: Optional[str] = None,
        domain: Optional[str] = None,
        ticker: Optional[str] = None,
        linkedin_url: Optional[str] = None,
        location: Optional[Dict[str, str]] = None,
        min_likelihood: int = 0,
        pretty: bool = True,
    ) -> Dict[str, Any]:
        """Enrich a single company.

        Returns enriched company data including:
        - Basic info (name, website, industry, size, etc.)
        - Financials (revenue, funding, etc.)
        - Technographics
        - Social profiles
        - And more
        """
        if not self.available():
            return {"error": "PDL API key not configured"}

        params = {}
        if name:
            params["name"] = name
        if website:
            params["website"] = website
        if domain:
            params["domain"] = domain
        if ticker:
            params["ticker"] = ticker
        if linkedin_url:
            params["linkedin_url"] = linkedin_url
        if location:
            params["location"] = location
        if min_likelihood > 0:
            params["min_likelihood"] = min_likelihood
        params["pretty"] = pretty

        # Try cache first
        key = self._company_key_from_params(params)
        cached = self._cache_get(self._company_cache, key)
        if cached is not None:
            return cached

        data = self._request("GET", "/company/enrich", params=params)
        # Cache only successful matches that include data
        if (
            data
            and isinstance(data, dict)
            and data.get("status") in (200, None)
            and data.get("data")
        ):
            self._cache_put(self._company_cache, key, data)
        return data

    def enrich_person(
        self,
        email: Optional[str] = None,
        phone: Optional[str] = None,
        name: Optional[Union[str, List[str]]] = None,
        first_name: Optional[str] = None,
        last_name: Optional[str] = None,
        company: Optional[str] = None,
        linkedin_url: Optional[str] = None,
        location: Optional[Dict[str, str]] = None,
        min_likelihood: int = 0,
        required: Optional[str] = None,
        pretty: bool = True,
    ) -> Dict[str, Any]:
        """Enrich a single person.

        Returns enriched person data including:
        - Contact info (emails, phones, social profiles)
        - Work history and current position
        - Education
        - Skills and interests
        - And more
        """
        if not self.available():
            return {"error": "PDL API key not configured"}

        params = {}
        if email:
            params["email"] = email
        if phone:
            params["phone"] = phone
        if name:
            params["name"] = name if isinstance(name, list) else [name]
        if first_name:
            params["first_name"] = first_name
        if last_name:
            params["last_name"] = last_name
        if company:
            params["company"] = company
        if linkedin_url:
            params["profile"] = linkedin_url
        if location:
            params["location"] = location
        if min_likelihood > 0:
            params["min_likelihood"] = min_likelihood
        if required:
            params["required"] = required
        params["pretty"] = pretty

        # Try cache first
        key = self._person_key_from_params(params)
        cached = self._cache_get(self._person_cache, key)
        if cached is not None:
            return cached

        data = self._request("GET", "/person/enrich", params=params)
        if (
            data
            and isinstance(data, dict)
            and data.get("status") in (200, None)
            and data.get("data")
        ):
            self._cache_put(self._person_cache, key, data)
        return data

    def bulk_company_enrichment(
        self, requests: List[PDLCompanyEnrichmentRequest]
    ) -> Dict[str, Any]:
        """Bulk company enrichment (up to 100 at once).

        Returns a list of enriched company records.
        Note: Bulk endpoints have better pricing per-record.
        """
        if not self.available():
            return {"error": "PDL API key not configured"}

        if len(requests) > 100:
            return {"error": "Maximum 100 requests per bulk call"}

        # Deduplicate within request to save credits; map back to original order
        unique_payload, index_map = self._dedupe_company_requests(requests)

        # Check cache for unique requests first and split into cache hits/misses
        cached_responses: Dict[int, Dict[str, Any]] = {}
        missing_indices: List[int] = []
        for i, req_dict in enumerate(unique_payload):
            key = self._company_key_from_params(req_dict)
            cached = self._cache_get(self._company_cache, key)
            if cached is not None:
                cached_responses[i] = cached
            else:
                missing_indices.append(i)

        api_responses: List[Optional[Dict[str, Any]]] = [None] * len(unique_payload)
        # Fill cached
        for i, data in cached_responses.items():
            api_responses[i] = data

        # Call API for missing
        if missing_indices:
            # PDL bulk API expects {"params": {...}} wrapper for each request
            payload = {
                "requests": [{"params": unique_payload[i]} for i in missing_indices]
            }
            data = self._request("POST", "/company/bulk", json=payload)

            # Handle error responses
            if isinstance(data, dict) and "error" in data:
                return data

            # PDL bulk endpoint returns a list directly, not a dict with "responses" key
            if isinstance(data, list):
                resp_list = data
            elif isinstance(data, dict):
                resp_list = data.get("responses", [])
            else:
                resp_list = []
            for j, idx in enumerate(missing_indices):
                if j < len(resp_list):
                    api_responses[idx] = resp_list[j]
                    # Cache successful
                    key = self._company_key_from_params(unique_payload[idx])
                    if api_responses[idx] and api_responses[idx].get("data"):
                        self._cache_put(self._company_cache, key, api_responses[idx])

        # Map back to original length
        full_responses = self._expand_responses_from_index_map(index_map, api_responses)
        return {"responses": full_responses}

    def bulk_person_enrichment(
        self, requests: List[PDLPersonEnrichmentRequest]
    ) -> Dict[str, Any]:
        """Bulk person enrichment (up to 100 at once).

        Returns a list of enriched person records.
        Note: Bulk endpoints have better pricing per-record.
        """
        if not self.available():
            return {"error": "PDL API key not configured"}

        if len(requests) > 100:
            return {"error": "Maximum 100 requests per bulk call"}

        # Deduplicate within request to save credits; map back to original order
        unique_payload, index_map = self._dedupe_person_requests(requests)

        cached_responses, missing_indices = self._inspect_person_cache(unique_payload)
        api_responses: List[Optional[Dict[str, Any]]] = [None] * len(unique_payload)
        for i, data in cached_responses.items():
            api_responses[i] = data

        requests_made = len(missing_indices)
        cache_hits = len(cached_responses)

        if missing_indices:
            # PDL bulk API expects {"params": {...}} wrapper for each request
            payload = {
                "requests": [{"params": unique_payload[i]} for i in missing_indices]
            }

            # Log first few requests for debugging
            log.info(
                f"PDL bulk person enrichment: sending {len(payload['requests'])} requests"
            )
            if payload["requests"]:
                log.debug(
                    f"Sample request payload (first 3): {payload['requests'][:3]}"
                )

            data = self._request("POST", "/person/bulk", json=payload)

            # Log response details
            log.info(
                f"PDL response type: {type(data).__name__}, length: {len(data) if isinstance(data, (list, dict)) else 'N/A'}"
            )
            if isinstance(data, list) and data:
                log.debug(f"Sample response (first 3): {data[:3]}")
            elif isinstance(data, dict):
                log.debug(f"Response dict keys: {list(data.keys())}")

            # Handle error responses
            if isinstance(data, dict) and "error" in data:
                log.error(f"PDL API returned error: {data['error']}")
                return data

            # PDL bulk endpoint returns a list directly, not a dict with "responses" key
            if isinstance(data, list):
                resp_list = data
            elif isinstance(data, dict):
                resp_list = data.get("responses", [])
            else:
                resp_list = []
            for j, idx in enumerate(missing_indices):
                if j < len(resp_list):
                    api_responses[idx] = resp_list[j]
                else:
                    api_responses[idx] = {}

                key = self._person_key_from_params(unique_payload[idx])
                cached_value = api_responses[idx]
                if cached_value and cached_value.get("data"):
                    self._cache_put(self._person_cache, key, cached_value)

        full_responses = self._expand_responses_from_index_map(index_map, api_responses)
        return {
            "responses": full_responses,
            "meta": {
                "unique_candidates": len(unique_payload),
                "cache_hits": cache_hits,
                "requests_made": requests_made,
            },
        }

    def search_companies(
        self,
        sql_query: Optional[str] = None,
        es_query: Optional[Dict] = None,
        size: int = 10,
        from_: int = 0,
        pretty: bool = True,
    ) -> Dict[str, Any]:
        """Search for companies using SQL or Elasticsearch query.

        Example SQL query:
        "SELECT * FROM company WHERE industry = 'software' AND employee_count > 100"

        Returns matching companies with full enrichment data.
        """
        if not self.available():
            return {"error": "PDL API key not configured"}

        params = {"size": size, "from": from_, "pretty": pretty}

        if sql_query:
            params["sql"] = sql_query
        elif es_query:
            params["query"] = es_query
        else:
            return {"error": "Must provide either sql_query or es_query"}

        return self._request(
            "POST",
            "/company/search",
            json=params if es_query else None,
            params=params if sql_query else None,
        )

    def search_people(
        self,
        sql_query: Optional[str] = None,
        es_query: Optional[Dict] = None,
        size: int = 10,
        from_: int = 0,
        pretty: bool = True,
    ) -> Dict[str, Any]:
        """Search for people using SQL or Elasticsearch query.

        Example SQL query:
        "SELECT * FROM person WHERE job_title CONTAINS 'ceo' AND location_country = 'united states'"

        Returns matching people with full enrichment data.
        """
        if not self.available():
            return {"error": "PDL API key not configured"}

        params = {"size": size, "from": from_, "pretty": pretty}

        if sql_query:
            params["sql"] = sql_query
        elif es_query:
            params["query"] = es_query
        else:
            return {"error": "Must provide either sql_query or es_query"}

        return self._request(
            "POST",
            "/person/search",
            json=params if es_query else None,
            params=params if sql_query else None,
        )

    def get_job_postings(
        self,
        company_name: Optional[str] = None,
        company_domain: Optional[str] = None,
        job_title: Optional[str] = None,
        location: Optional[str] = None,
        size: int = 10,
    ) -> Dict[str, Any]:
        """Get job postings data for market intelligence.

        Useful for:
        - Understanding hiring trends
        - Identifying growing companies
        - Competitive intelligence
        """
        if not self.available():
            return {"error": "PDL API key not configured"}

        sql_parts = []
        if company_name:
            sql_parts.append(f"company_name = '{company_name}'")
        if company_domain:
            sql_parts.append(f"company_domain = '{company_domain}'")
        if job_title:
            sql_parts.append(f"job_title CONTAINS '{job_title}'")
        if location:
            sql_parts.append(f"location CONTAINS '{location}'")

        if not sql_parts:
            return {"error": "Must provide at least one search criterion"}

        sql_query = f"SELECT * FROM job WHERE {' AND '.join(sql_parts)} LIMIT {size}"

        return self._request(
            "GET",
            "/job/search",
            params={"sql": sql_query, "pretty": True},
        )

    def enrich_batch(
        self,
        headers: List[str],
        rows: List[List[Any]],
        fields: List[str],
        enrich_type: str = "company",
    ) -> tuple[List[List[Any]], float]:
        """Batch enrichment compatible with existing Apollo pattern.

        Maps spreadsheet data to PDL enrichment requests.
        Returns (enriched_rows, credits_used).
        """
        if not self.available():
            return rows, 0.0

        # Map headers to indices
        header_map = {h.lower(): i for i, h in enumerate(headers)}

        # Prepare batch requests based on type
        if enrich_type == "company":
            requests = []
            for row in rows[:100]:  # PDL limit is 100 per batch
                req = PDLCompanyEnrichmentRequest()

                # Map common fields
                if "company" in header_map:
                    req.name = row[header_map["company"]]
                elif "company_name" in header_map:
                    req.name = row[header_map["company_name"]]

                if "domain" in header_map:
                    req.domain = row[header_map["domain"]]
                elif "website" in header_map:
                    req.website = row[header_map["website"]]

                if "linkedin" in header_map:
                    req.linkedin_url = row[header_map["linkedin"]]
                elif "company_linkedin" in header_map:
                    req.linkedin_url = row[header_map["company_linkedin"]]

                requests.append(req)

            result = self.bulk_company_enrichment(requests)

        else:  # person enrichment
            requests = []
            for row in rows[:100]:
                req = PDLPersonEnrichmentRequest()

                # Map common fields
                if "email" in header_map:
                    req.email = row[header_map["email"]]

                if "first_name" in header_map:
                    req.first_name = row[header_map["first_name"]]

                if "last_name" in header_map:
                    req.last_name = row[header_map["last_name"]]

                if "company" in header_map:
                    req.company = row[header_map["company"]]
                elif "company_name" in header_map:
                    req.company = row[header_map["company_name"]]

                if "linkedin" in header_map:
                    req.profile = row[header_map["linkedin"]]
                elif "linkedin_url" in header_map:
                    req.profile = row[header_map["linkedin_url"]]

                if "phone" in header_map:
                    req.phone = row[header_map["phone"]]

                requests.append(req)

            result = self.bulk_person_enrichment(requests)

        # Process results and merge back with original rows
        if "error" in result:
            return rows, 0.0

        enriched_rows = []
        responses = result.get("responses", [])

        # Build new headers with PDL fields
        new_headers = list(headers)
        pdl_fields = set()

        for resp in responses:
            if resp and "data" in resp:
                for key in resp["data"].keys():
                    pdl_field = f"pdl_{key}"
                    if pdl_field not in new_headers:
                        new_headers.append(pdl_field)
                        pdl_fields.add(pdl_field)

        # Merge enriched data with original rows
        for i, row in enumerate(rows[: len(responses)]):
            new_row = list(row) + [""] * len(pdl_fields)

            if i < len(responses) and responses[i] and "data" in responses[i]:
                data = responses[i]["data"]
                for key, value in data.items():
                    pdl_field = f"pdl_{key}"
                    if pdl_field in new_headers:
                        idx = new_headers.index(pdl_field)
                        if idx < len(new_row):
                            # Handle nested objects by converting to string
                            if isinstance(value, (dict, list)):
                                new_row[idx] = str(value)
                            else:
                                new_row[idx] = value

            enriched_rows.append(new_row)

        # Add remaining rows that weren't enriched
        for row in rows[len(responses) :]:
            enriched_rows.append(list(row) + [""] * len(pdl_fields))

        # Estimate credits used (PDL charges per successful match)
        credits = len(
            [
                r
                for r in responses
                if r and "data" in r and (r.get("status") in (200, None))
            ]
        )

        return enriched_rows, float(credits)

    # ---- Internal helpers: caching, dedupe, keys ----
    def _cache_get(
        self, cache: Dict[str, Tuple[float, Dict[str, Any]]], key: str
    ) -> Optional[Dict[str, Any]]:
        if not key:
            return None
        now = time.time()
        with self._cache_lock:
            item = cache.get(key)
            if not item:
                return None
            exp, data = item
            if exp < now:
                # expired
                try:
                    del cache[key]
                except Exception:
                    pass
                return None
            return data

    def _cache_put(
        self,
        cache: Dict[str, Tuple[float, Dict[str, Any]]],
        key: str,
        data: Dict[str, Any],
    ) -> None:
        if not key or not isinstance(data, dict):
            return
        now = time.time()
        with self._cache_lock:
            # size management (simple random eviction when over size)
            if len(cache) >= max(100, self._cache_size):
                for _ in range(5):
                    try:
                        cache.pop(next(iter(cache)))
                    except Exception:
                        break
            cache[key] = (now + max(60, self._cache_ttl), data)

    # Build keys
    @staticmethod
    def _norm_domain(domain: Optional[str]) -> str:
        if not domain:
            return ""
        d = str(domain).strip().lower()
        if d.startswith("http://") or d.startswith("https://"):
            try:
                # crude parse
                d = d.split("://", 1)[1]
            except Exception:
                pass
        if d.startswith("www."):
            d = d[4:]
        # strip path
        d = d.split("/")[0]
        return d

    @staticmethod
    def _company_key_from_params(params: Dict[str, Any]) -> str:
        if not params:
            return ""
        d = PeopleDataLabsAdapter._norm_domain(
            params.get("domain") or params.get("website")
        )
        if d:
            return f"d:{d}"
        ln = str(params.get("linkedin_url") or "").strip().lower()
        if ln:
            return f"ln:{ln}"
        name = str(params.get("name") or "").strip().lower()
        loc = params.get("location") or {}
        city = str((loc or {}).get("locality") or "").strip().lower()
        country = str((loc or {}).get("country") or "").strip().lower()
        return f"n:{name}|c:{city}|cc:{country}"

    @staticmethod
    def _person_key_from_params(params: Dict[str, Any]) -> str:
        if not params:
            return ""
        email = str(params.get("email") or "").strip().lower()
        if email:
            return f"e:{email}"
        prof = str(params.get("profile") or "").strip().lower()
        if prof:
            return f"p:{prof}"
        fn = str(params.get("first_name") or "").strip().lower()
        ln = str(params.get("last_name") or "").strip().lower()
        co = str(params.get("company") or "").strip().lower()
        return f"n:{fn} {ln}|co:{co}"

    def _dedupe_company_requests(
        self, requests: List[PDLCompanyEnrichmentRequest]
    ) -> Tuple[List[Dict[str, Any]], List[int]]:
        unique: List[Dict[str, Any]] = []
        index_map: List[int] = []  # maps original index -> unique index
        seen: Dict[str, int] = {}
        for i, req in enumerate(requests):
            req_dict = {k: v for k, v in asdict(req).items() if v is not None}
            key = self._company_key_from_params(req_dict)
            if key and key in seen:
                index_map.append(seen[key])
            else:
                seen[key] = len(unique)
                index_map.append(seen[key])
                unique.append(req_dict)
        return unique, index_map

    def _dedupe_person_requests(
        self, requests: List[PDLPersonEnrichmentRequest]
    ) -> Tuple[List[Dict[str, Any]], List[int]]:
        unique: List[Dict[str, Any]] = []
        index_map: List[int] = []
        seen: Dict[str, int] = {}
        for i, req in enumerate(requests):
            req_dict = {k: v for k, v in asdict(req).items() if v is not None}
            key = self._person_key_from_params(req_dict)
            if key and key in seen:
                index_map.append(seen[key])
            else:
                seen[key] = len(unique)
                index_map.append(seen[key])
                unique.append(req_dict)
        return unique, index_map

    def _inspect_person_cache(
        self,
        unique_payload: List[Dict[str, Any]],
    ) -> Tuple[Dict[int, Dict[str, Any]], List[int]]:
        cached_responses: Dict[int, Dict[str, Any]] = {}
        missing_indices: List[int] = []
        for i, req_dict in enumerate(unique_payload):
            key = self._person_key_from_params(req_dict)
            cached = self._cache_get(self._person_cache, key)
            if cached is not None:
                cached_responses[i] = cached
            else:
                missing_indices.append(i)
        return cached_responses, missing_indices

    def estimate_person_requests(
        self,
        requests: List[PDLPersonEnrichmentRequest],
    ) -> Dict[str, int]:
        if not requests:
            return {
                "total_candidates": 0,
                "unique_candidates": 0,
                "cache_hits": 0,
                "requests_needed": 0,
            }

        unique_payload, _ = self._dedupe_person_requests(requests)
        cached_responses, missing_indices = self._inspect_person_cache(unique_payload)
        return {
            "total_candidates": len(requests),
            "unique_candidates": len(unique_payload),
            "cache_hits": len(cached_responses),
            "requests_needed": len(missing_indices),
        }

    @staticmethod
    def _expand_responses_from_index_map(
        index_map: List[int], unique_responses: List[Optional[Dict[str, Any]]]
    ) -> List[Dict[str, Any]]:
        out: List[Dict[str, Any]] = []
        for idx in index_map:
            r = unique_responses[idx]
            out.append(r or {})
        return out
