import re
import xml.etree.ElementTree as ET
from typing import Any, Dict, List, Optional, Tuple

from langchain_core.language_models import BaseLLM
from loguru import logger

from ...config import search_config
from ...security.safe_requests import safe_get
from ..rate_limiting import RateLimitError
from ..search_engine_base import BaseSearchEngine


class PubMedSearchEngine(BaseSearchEngine):
    """
    PubMed search engine implementation with two-phase approach and adaptive search.
    Provides efficient access to biomedical literature while minimizing API usage.
    """

    # Mark as public search engine
    is_public = True
    # Scientific/medical search engine
    is_scientific = True

    def __init__(
        self,
        max_results: int = 10,
        api_key: Optional[str] = None,
        days_limit: Optional[int] = None,
        get_abstracts: bool = True,
        get_full_text: bool = False,
        full_text_limit: int = 3,
        llm: Optional[BaseLLM] = None,
        max_filtered_results: Optional[int] = None,
        optimize_queries: bool = True,
        include_publication_type_in_context: bool = True,
        include_journal_in_context: bool = True,
        include_year_in_context: bool = True,
        include_authors_in_context: bool = False,
        include_full_date_in_context: bool = False,
        include_mesh_terms_in_context: bool = True,
        include_keywords_in_context: bool = True,
        include_doi_in_context: bool = False,
        include_pmid_in_context: bool = False,
        include_pmc_availability_in_context: bool = False,
        max_mesh_terms: int = 3,
        max_keywords: int = 3,
        include_citation_in_context: bool = False,
        include_language_in_context: bool = False,
    ):
        """
        Initialize the PubMed search engine.

        Args:
            max_results: Maximum number of search results
            api_key: NCBI API key for higher rate limits (optional)
            days_limit: Limit results to N days (optional)
            get_abstracts: Whether to fetch abstracts for all results
            get_full_text: Whether to fetch full text content (when available in PMC)
            full_text_limit: Max number of full-text articles to retrieve
            llm: Language model for relevance filtering
            max_filtered_results: Maximum number of results to keep after filtering
            optimize_queries: Whether to optimize natural language queries for PubMed
        """
        # Initialize the BaseSearchEngine with LLM, max_filtered_results, and max_results
        super().__init__(
            llm=llm,
            max_filtered_results=max_filtered_results,
            max_results=max_results,
        )
        self.max_results = max(self.max_results, 25)
        self.api_key = api_key
        self.days_limit = days_limit
        self.get_abstracts = get_abstracts
        self.get_full_text = get_full_text
        self.full_text_limit = full_text_limit
        self.optimize_queries = optimize_queries
        self.include_publication_type_in_context = (
            include_publication_type_in_context
        )
        self.include_journal_in_context = include_journal_in_context
        self.include_year_in_context = include_year_in_context
        self.include_authors_in_context = include_authors_in_context
        self.include_full_date_in_context = include_full_date_in_context
        self.include_mesh_terms_in_context = include_mesh_terms_in_context
        self.include_keywords_in_context = include_keywords_in_context
        self.include_doi_in_context = include_doi_in_context
        self.include_pmid_in_context = include_pmid_in_context
        self.include_pmc_availability_in_context = (
            include_pmc_availability_in_context
        )
        self.max_mesh_terms = max_mesh_terms
        self.max_keywords = max_keywords
        self.include_citation_in_context = include_citation_in_context
        self.include_language_in_context = include_language_in_context

        # Base API URLs
        self.base_url = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils"
        self.search_url = f"{self.base_url}/esearch.fcgi"
        self.summary_url = f"{self.base_url}/esummary.fcgi"
        self.fetch_url = f"{self.base_url}/efetch.fcgi"
        self.link_url = f"{self.base_url}/elink.fcgi"

        # PMC base URL for full text
        self.pmc_url = "https://www.ncbi.nlm.nih.gov/pmc/articles/"

    def _get_result_count(self, query: str) -> int:
        """
        Get the total number of results for a query without retrieving the results themselves.

        Args:
            query: The search query

        Returns:
            Total number of matching results
        """
        try:
            # Prepare search parameters
            params = {
                "db": "pubmed",
                "term": query,
                "retmode": "json",
                "retmax": 0,  # Don't need actual results, just the count
            }

            # Add API key if available
            if self.api_key:
                params["api_key"] = self.api_key

            self._last_wait_time = self.rate_tracker.apply_rate_limit(
                self.engine_type
            )

            # Execute search request
            response = safe_get(self.search_url, params=params)
            response.raise_for_status()

            # Parse response
            data = response.json()
            count = int(data["esearchresult"]["count"])

            logger.info(
                "Query '%s' has %s total results in PubMed", query, count
            )
            return count

        except Exception:
            logger.exception("Error getting result count")
            return 0

    def _extract_core_terms(self, query: str) -> str:
        """
        Extract core terms from a complex query for volume estimation.

        Args:
            query: PubMed query string

        Returns:
            Simplified query with core terms
        """
        # Remove field specifications and operators
        simplified = re.sub(r"\[\w+\]", "", query)  # Remove [Field] tags
        simplified = re.sub(
            r"\b(AND|OR|NOT)\b", "", simplified
        )  # Remove operators

        # Remove quotes and parentheses
        simplified = (
            simplified.replace('"', "").replace("(", "").replace(")", "")
        )

        # Split by whitespace and join terms with 4+ chars (likely meaningful)
        terms = [term for term in simplified.split() if len(term) >= 4]

        # Join with AND to create a basic search
        return " ".join(terms[:5])  # Limit to top 5 terms

    def _expand_time_window(self, time_filter: str) -> str:
        """
        Expand a time window to get more results.

        Args:
            time_filter: Current time filter

        Returns:
            Expanded time filter
        """
        # Parse current time window
        import re

        match = re.match(r'"last (\d+) (\w+)"[pdat]', time_filter)
        if not match:
            return '"last 10 years"[pdat]'

        amount, unit = int(match.group(1)), match.group(2)

        # Expand based on current unit
        if unit == "months" or unit == "month":
            if amount < 6:
                return '"last 6 months"[pdat]'
            elif amount < 12:
                return '"last 1 year"[pdat]'
            else:
                return '"last 2 years"[pdat]'
        elif unit == "years" or unit == "year":
            if amount < 2:
                return '"last 2 years"[pdat]'
            elif amount < 5:
                return '"last 5 years"[pdat]'
            else:
                return '"last 10 years"[pdat]'

        return '"last 10 years"[pdat]'

    def _optimize_query_for_pubmed(self, query: str) -> str:
        """
        Optimize a natural language query for PubMed search.
        Uses LLM to transform questions into effective keyword-based queries.

        Args:
            query: Natural language query

        Returns:
            Optimized query string for PubMed
        """
        if not self.llm or not self.optimize_queries:
            # Return original query if no LLM available or optimization disabled
            return query

        try:
            # Prompt for query optimization
            prompt = f"""Transform this natural language question into an optimized PubMed search query.

Original query: "{query}"

CRITICAL RULES:
1. ONLY RETURN THE EXACT SEARCH QUERY - NO EXPLANATIONS, NO COMMENTS
2. DO NOT wrap the entire query in quotes
3. DO NOT include ANY date restrictions or year filters
4. Use parentheses around OR statements: (term1[Field] OR term2[Field])
5. Use only BASIC MeSH terms - stick to broad categories like "Vaccines"[Mesh]
6. KEEP IT SIMPLE - use 2-3 main concepts maximum
7. Focus on Title/Abstract searches for reliability: term[Title/Abstract]
8. Use wildcards for variations: vaccin*[Title/Abstract]

EXAMPLE QUERIES:
✓ GOOD: (mRNA[Title/Abstract] OR "messenger RNA"[Title/Abstract]) AND vaccin*[Title/Abstract]
✓ GOOD: (influenza[Title/Abstract] OR flu[Title/Abstract]) AND treatment[Title/Abstract]
✗ BAD: (mRNA[Title/Abstract]) AND "specific disease"[Mesh] AND treatment[Title/Abstract] AND 2023[dp]
✗ BAD: "Here's a query to find articles about vaccines..."

Return ONLY the search query without any explanations.
"""

            # Get response from LLM
            response = self.llm.invoke(prompt)
            raw_response = response.content.strip()

            # Clean up the query - extract only the actual query and remove any explanations
            # First check if there are multiple lines and take the first non-empty line
            lines = raw_response.split("\n")
            cleaned_lines = [line.strip() for line in lines if line.strip()]

            if cleaned_lines:
                optimized_query = cleaned_lines[0]

                # Remove any quotes that wrap the entire query
                if optimized_query.startswith('"') and optimized_query.endswith(
                    '"'
                ):
                    optimized_query = optimized_query[1:-1]

                # Remove any explanation phrases that might be at the beginning
                explanation_starters = [
                    "here is",
                    "here's",
                    "this query",
                    "the following",
                ]
                for starter in explanation_starters:
                    if optimized_query.lower().startswith(starter):
                        # Find the actual query part - typically after a colon
                        colon_pos = optimized_query.find(":")
                        if colon_pos > 0:
                            optimized_query = optimized_query[
                                colon_pos + 1 :
                            ].strip()

                # Check if the query still seems to contain explanations
                if (
                    len(optimized_query) > 200
                    or "this query will" in optimized_query.lower()
                ):
                    # It's probably still an explanation - try to extract just the query part
                    # Look for common patterns in the explanation like parentheses
                    pattern = r"\([^)]+\)\s+AND\s+"
                    import re

                    matches = re.findall(pattern, optimized_query)
                    if matches:
                        # Extract just the query syntax parts
                        query_parts = []
                        for part in re.split(r"\.\s+", optimized_query):
                            if (
                                "(" in part
                                and ")" in part
                                and ("AND" in part or "OR" in part)
                            ):
                                query_parts.append(part)
                        if query_parts:
                            optimized_query = " ".join(query_parts)
            else:
                # Fall back to original query if cleaning fails
                logger.warning(
                    "Failed to extract a clean query from LLM response"
                )
                optimized_query = query

            # Final safety check - if query looks too much like an explanation, use original
            if len(optimized_query.split()) > 30:
                logger.warning(
                    "Query too verbose, falling back to simpler form"
                )
                # Create a simple query from the original
                words = [
                    w
                    for w in query.split()
                    if len(w) > 3
                    and w.lower()
                    not in (
                        "what",
                        "are",
                        "the",
                        "and",
                        "for",
                        "with",
                        "from",
                        "have",
                        "been",
                        "recent",
                    )
                ]
                optimized_query = " AND ".join(words[:3])

            # Basic cleanup: standardize field tag case for consistency
            import re

            optimized_query = re.sub(
                r"\[mesh\]", "[Mesh]", optimized_query, flags=re.IGNORECASE
            )
            optimized_query = re.sub(
                r"\[title/abstract\]",
                "[Title/Abstract]",
                optimized_query,
                flags=re.IGNORECASE,
            )
            optimized_query = re.sub(
                r"\[publication type\]",
                "[Publication Type]",
                optimized_query,
                flags=re.IGNORECASE,
            )

            # Fix unclosed quotes followed by field tags
            # Pattern: "term[Field] -> "term"[Field]
            optimized_query = re.sub(r'"([^"]+)\[', r'"\1"[', optimized_query)

            # Simplify the query if still no results are found
            self._simplify_query_cache = optimized_query

            # Log original and optimized queries
            logger.info("Original query: '%s'", query)
            logger.info(f"Optimized for PubMed: '{optimized_query}'")
            logger.debug(
                f"Query optimization complete: '{query[:50]}...' -> '{optimized_query[:100]}...'"
            )

            return optimized_query

        except Exception:
            logger.exception("Error optimizing query")
            logger.debug(f"Falling back to original query: '{query}'")
            return query  # Fall back to original query on error

    def _simplify_query(self, query: str) -> str:
        """
        Simplify a PubMed query that returned no results.
        Progressively removes elements to get a more basic query.

        Args:
            query: The original query that returned no results

        Returns:
            Simplified query
        """
        logger.info(f"Simplifying query: {query}")
        logger.debug(f"Query simplification started for: '{query[:100]}...'")

        # Simple approach: remove field restrictions to broaden the search
        import re

        # Remove field tags to make search broader
        simplified = query

        # Remove [Mesh] tags - search in all fields instead
        simplified = re.sub(r"\[Mesh\]", "", simplified, flags=re.IGNORECASE)

        # Remove [Publication Type] tags
        simplified = re.sub(
            r"\[Publication Type\]", "", simplified, flags=re.IGNORECASE
        )

        # Keep [Title/Abstract] as it's usually helpful
        # Clean up any double spaces
        simplified = re.sub(r"\s+", " ", simplified).strip()

        # If no simplification was possible, return the original query
        if simplified == query:
            logger.debug("No simplification possible, returning original query")

        logger.info(f"Simplified query: {simplified}")
        logger.debug(
            f"Query simplified from {len(query)} to {len(simplified)} chars"
        )
        return simplified

    def _is_historical_focused(self, query: str) -> bool:
        """
        Determine if a query is specifically focused on historical/older information using LLM.
        Default assumption is that queries should prioritize recent information unless
        explicitly asking for historical content.

        Args:
            query: The search query

        Returns:
            Boolean indicating if the query is focused on historical information
        """
        if not self.llm:
            # Fall back to basic keyword check if no LLM available
            historical_terms = [
                "history",
                "historical",
                "early",
                "initial",
                "first",
                "original",
                "before",
                "prior to",
                "origins",
                "evolution",
                "development",
            ]
            historical_years = [str(year) for year in range(1900, 2020)]

            query_lower = query.lower()
            has_historical_term = any(
                term in query_lower for term in historical_terms
            )
            has_past_year = any(year in query for year in historical_years)

            return has_historical_term or has_past_year

        try:
            # Use LLM to determine if the query is focused on historical information
            prompt = f"""Determine if this query is specifically asking for HISTORICAL or OLDER information.

Query: "{query}"

Answer ONLY "yes" if the query is clearly asking for historical, early, original, or past information from more than 5 years ago.
Answer ONLY "no" if the query is asking about recent, current, or new information, or if it's a general query without a specific time focus.

The default assumption should be that medical and scientific queries want RECENT information unless clearly specified otherwise.
"""

            response = self.llm.invoke(prompt)
            answer = response.content.strip().lower()

            # Log the determination
            logger.info(f"Historical focus determination for query: '{query}'")
            logger.info(f"LLM determined historical focus: {answer}")

            return "yes" in answer

        except Exception:
            logger.exception("Error determining historical focus")
            # Fall back to basic keyword check
            historical_terms = [
                "history",
                "historical",
                "early",
                "initial",
                "first",
                "original",
                "before",
                "prior to",
                "origins",
                "evolution",
                "development",
            ]
            return any(term in query.lower() for term in historical_terms)

    def _adaptive_search(self, query: str) -> Tuple[List[str], str]:
        """
        Perform an adaptive search that adjusts based on topic volume and whether
        the query focuses on historical information.

        Args:
            query: The search query (already optimized)

        Returns:
            Tuple of (list of PMIDs, search strategy used)
        """
        # Estimate topic volume
        estimated_volume = self._get_result_count(query)

        # Determine if the query is focused on historical information
        is_historical_focused = self._is_historical_focused(query)

        if is_historical_focused:
            # User wants historical information - no date filtering
            time_filter = None
            strategy = "historical_focus"
        elif estimated_volume > 5000:
            # Very common topic - use tighter recency filter
            time_filter = '"last 1 year"[pdat]'
            strategy = "high_volume"
        elif estimated_volume > 1000:
            # Common topic
            time_filter = '"last 3 years"[pdat]'
            strategy = "common_topic"
        elif estimated_volume > 100:
            # Moderate volume
            time_filter = '"last 5 years"[pdat]'
            strategy = "moderate_volume"
        else:
            # Rare topic - still use recency but with wider range
            time_filter = '"last 10 years"[pdat]'
            strategy = "rare_topic"

        # Run search based on strategy
        if time_filter:
            # Try with adaptive time filter
            query_with_time = f"({query}) AND {time_filter}"
            logger.info(
                f"Using adaptive search strategy: {strategy} with filter: {time_filter}"
            )
            results = self._search_pubmed(query_with_time)

            # If too few results, gradually expand time window
            if len(results) < 5 and '"last 10 years"[pdat]' not in time_filter:
                logger.info(
                    f"Insufficient results ({len(results)}), expanding time window"
                )
                expanded_time = self._expand_time_window(time_filter)
                query_with_expanded_time = f"({query}) AND {expanded_time}"
                expanded_results = self._search_pubmed(query_with_expanded_time)

                if len(expanded_results) > len(results):
                    logger.info(
                        f"Expanded time window yielded {len(expanded_results)} results"
                    )
                    return expanded_results, f"{strategy}_expanded"

            # If still no results, try without time filter
            if not results:
                logger.info(
                    "No results with time filter, trying without time restrictions"
                )
                results = self._search_pubmed(query)
                strategy = "no_time_filter"
        else:
            # Historical query - run without time filter
            logger.info(
                "Using historical search strategy without date filtering"
            )
            results = self._search_pubmed(query)

        return results, strategy

    def _search_pubmed(self, query: str) -> List[str]:
        """
        Search PubMed and return a list of article IDs.

        Args:
            query: The search query

        Returns:
            List of PubMed IDs matching the query
        """
        try:
            # Prepare search parameters
            params = {
                "db": "pubmed",
                "term": query,
                "retmode": "json",
                "retmax": self.max_results,
                "usehistory": "y",
            }

            # Add API key if available
            if self.api_key:
                params["api_key"] = self.api_key
                logger.debug("Using PubMed API key for higher rate limits")
            else:
                logger.debug("No PubMed API key - using default rate limits")

            # Add date restriction if specified
            if self.days_limit:
                params["reldate"] = self.days_limit
                params["datetype"] = "pdat"  # Publication date
                logger.debug(f"Limiting results to last {self.days_limit} days")

            logger.debug(
                f"PubMed search query: '{query}' with max_results={self.max_results}"
            )

            self._last_wait_time = self.rate_tracker.apply_rate_limit(
                self.engine_type
            )
            logger.debug(
                f"Applied rate limit wait: {self._last_wait_time:.2f}s"
            )

            # Execute search request
            logger.debug(f"Sending request to PubMed API: {self.search_url}")
            response = safe_get(self.search_url, params=params)
            response.raise_for_status()
            logger.debug(f"PubMed API response status: {response.status_code}")

            # Parse response
            data = response.json()
            id_list = data["esearchresult"]["idlist"]
            total_count = data["esearchresult"].get("count", "unknown")

            logger.info(
                f"PubMed search for '{query}' found {len(id_list)} results (total available: {total_count})"
            )
            if len(id_list) > 0:
                logger.debug(f"First 5 PMIDs: {id_list[:5]}")
            return id_list

        except Exception:
            logger.exception(f"Error searching PubMed for query '{query}'")
            return []

    def _get_article_summaries(
        self, id_list: List[str]
    ) -> List[Dict[str, Any]]:
        """
        Get summaries for a list of PubMed article IDs.

        Args:
            id_list: List of PubMed IDs

        Returns:
            List of article summary dictionaries
        """
        if not id_list:
            logger.debug("Empty ID list provided to _get_article_summaries")
            return []

        logger.debug(f"Fetching summaries for {len(id_list)} PubMed articles")

        try:
            # Prepare parameters
            params = {
                "db": "pubmed",
                "id": ",".join(id_list),
                "retmode": "json",
                "rettype": "summary",
            }

            # Add API key if available
            if self.api_key:
                params["api_key"] = self.api_key

            self._last_wait_time = self.rate_tracker.apply_rate_limit(
                self.engine_type
            )
            logger.debug(
                f"Applied rate limit wait: {self._last_wait_time:.2f}s"
            )

            # Execute request
            logger.debug(f"Requesting summaries from: {self.summary_url}")
            response = safe_get(self.summary_url, params=params)
            response.raise_for_status()
            logger.debug(f"Summary API response status: {response.status_code}")

            # Parse response
            data = response.json()
            logger.debug(
                f"PubMed API returned data for {len(id_list)} requested IDs"
            )
            summaries = []

            for pmid in id_list:
                if pmid in data["result"]:
                    article = data["result"][pmid]
                    logger.debug(
                        f"Processing article {pmid}: {article.get('title', 'NO TITLE')[:50]}"
                    )

                    # Extract authors (if available)
                    authors = []
                    if "authors" in article:
                        authors = [
                            author["name"] for author in article["authors"]
                        ]

                    # Extract DOI from articleids if not in main field
                    doi = article.get("doi", "")
                    if not doi and "articleids" in article:
                        for aid in article["articleids"]:
                            if aid.get("idtype") == "doi":
                                doi = aid.get("value", "")
                                break

                    # Create summary dictionary with all available fields
                    summary = {
                        "id": pmid,
                        "title": article.get("title", ""),
                        "pubdate": article.get("pubdate", ""),
                        "epubdate": article.get("epubdate", ""),
                        "source": article.get("source", ""),
                        "authors": authors,
                        "lastauthor": article.get("lastauthor", ""),
                        "journal": article.get("fulljournalname", ""),
                        "volume": article.get("volume", ""),
                        "issue": article.get("issue", ""),
                        "pages": article.get("pages", ""),
                        "doi": doi,
                        "issn": article.get("issn", ""),
                        "essn": article.get("essn", ""),
                        "pubtype": article.get(
                            "pubtype", []
                        ),  # Publication types from esummary
                        "recordstatus": article.get("recordstatus", ""),
                        "lang": article.get("lang", []),
                        "pmcrefcount": article.get("pmcrefcount", None),
                        "link": f"https://pubmed.ncbi.nlm.nih.gov/{pmid}/",
                    }

                    summaries.append(summary)
                else:
                    logger.warning(
                        f"PMID {pmid} not found in PubMed API response"
                    )

            return summaries

        except Exception as e:
            error_msg = str(e)
            logger.exception(
                f"Error getting article summaries for {len(id_list)} articles"
            )

            # Check for rate limiting patterns
            if (
                "429" in error_msg
                or "too many requests" in error_msg.lower()
                or "rate limit" in error_msg.lower()
                or "service unavailable" in error_msg.lower()
                or "503" in error_msg
                or "403" in error_msg
            ):
                raise RateLimitError(f"PubMed rate limit hit: {error_msg}")

            return []

    def _get_article_abstracts(self, id_list: List[str]) -> Dict[str, str]:
        """
        Get abstracts for a list of PubMed article IDs.

        Args:
            id_list: List of PubMed IDs

        Returns:
            Dictionary mapping PubMed IDs to their abstracts
        """
        if not id_list:
            logger.debug("Empty ID list provided to _get_article_abstracts")
            return {}

        logger.debug(f"Fetching abstracts for {len(id_list)} PubMed articles")

        try:
            # Prepare parameters
            params = {
                "db": "pubmed",
                "id": ",".join(id_list),
                "retmode": "xml",
                "rettype": "abstract",
            }

            # Add API key if available
            if self.api_key:
                params["api_key"] = self.api_key

            self._last_wait_time = self.rate_tracker.apply_rate_limit(
                self.engine_type
            )
            logger.debug(
                f"Applied rate limit wait: {self._last_wait_time:.2f}s"
            )

            # Execute request
            logger.debug(f"Requesting abstracts from: {self.fetch_url}")
            response = safe_get(self.fetch_url, params=params)
            response.raise_for_status()
            logger.debug(
                f"Abstract fetch response status: {response.status_code}, size: {len(response.text)} bytes"
            )

            # Parse XML response
            root = ET.fromstring(response.text)
            logger.debug(
                f"Parsing abstracts from XML for {len(id_list)} articles"
            )

            # Extract abstracts
            abstracts = {}

            for article in root.findall(".//PubmedArticle"):
                pmid_elem = article.find(".//PMID")
                pmid = pmid_elem.text if pmid_elem is not None else None

                if pmid is None:
                    continue

                # Find abstract text
                abstract_text = ""
                abstract_elem = article.find(".//AbstractText")

                if abstract_elem is not None:
                    abstract_text = abstract_elem.text or ""

                # Some abstracts are split into multiple sections
                abstract_sections = article.findall(".//AbstractText")
                if len(abstract_sections) > 1:
                    logger.debug(
                        f"Article {pmid} has {len(abstract_sections)} abstract sections"
                    )

                for section in abstract_sections:
                    # Get section label if it exists
                    label = section.get("Label")
                    section_text = section.text or ""

                    if label and section_text:
                        if abstract_text:
                            abstract_text += f"\n\n{label}: {section_text}"
                        else:
                            abstract_text = f"{label}: {section_text}"
                    elif section_text:
                        if abstract_text:
                            abstract_text += f"\n\n{section_text}"
                        else:
                            abstract_text = section_text

                # Store in dictionary
                if pmid and abstract_text:
                    abstracts[pmid] = abstract_text
                    logger.debug(
                        f"Abstract for {pmid}: {len(abstract_text)} chars"
                    )
                elif pmid:
                    logger.warning(f"No abstract found for PMID {pmid}")

            logger.info(
                f"Successfully retrieved {len(abstracts)} abstracts out of {len(id_list)} requested"
            )
            return abstracts

        except Exception as e:
            logger.exception(
                f"Error getting article abstracts for {len(id_list)} articles: {str(e)}"
            )
            return {}

    def _get_article_detailed_metadata(
        self, id_list: List[str]
    ) -> Dict[str, Dict[str, Any]]:
        """
        Get detailed metadata for PubMed articles including publication types,
        MeSH terms, keywords, and affiliations.

        Args:
            id_list: List of PubMed IDs

        Returns:
            Dictionary mapping PubMed IDs to their detailed metadata
        """
        if not id_list:
            return {}

        try:
            # Prepare parameters
            params = {
                "db": "pubmed",
                "id": ",".join(id_list),
                "retmode": "xml",
                "rettype": "medline",
            }

            # Add API key if available
            if self.api_key:
                params["api_key"] = self.api_key

            self._last_wait_time = self.rate_tracker.apply_rate_limit(
                self.engine_type
            )

            # Execute request
            response = safe_get(self.fetch_url, params=params)
            response.raise_for_status()

            # Parse XML response
            root = ET.fromstring(response.text)

            metadata = {}

            for article in root.findall(".//PubmedArticle"):
                pmid_elem = article.find(".//PMID")
                pmid = pmid_elem.text if pmid_elem is not None else None

                if pmid is None:
                    continue

                article_metadata = {}

                # Extract publication types
                pub_types = []
                for pub_type in article.findall(".//PublicationType"):
                    if pub_type.text:
                        pub_types.append(pub_type.text)
                if pub_types:
                    article_metadata["publication_types"] = pub_types

                # Extract MeSH terms
                mesh_terms = []
                for mesh in article.findall(".//MeshHeading"):
                    descriptor = mesh.find(".//DescriptorName")
                    if descriptor is not None and descriptor.text:
                        mesh_terms.append(descriptor.text)
                if mesh_terms:
                    article_metadata["mesh_terms"] = mesh_terms

                # Extract keywords
                keywords = []
                for keyword in article.findall(".//Keyword"):
                    if keyword.text:
                        keywords.append(keyword.text)
                if keywords:
                    article_metadata["keywords"] = keywords

                # Extract affiliations
                affiliations = []
                for affiliation in article.findall(".//Affiliation"):
                    if affiliation.text:
                        affiliations.append(affiliation.text)
                if affiliations:
                    article_metadata["affiliations"] = affiliations

                # Extract grant information
                grants = []
                for grant in article.findall(".//Grant"):
                    grant_info = {}
                    grant_id = grant.find(".//GrantID")
                    if grant_id is not None and grant_id.text:
                        grant_info["id"] = grant_id.text
                    agency = grant.find(".//Agency")
                    if agency is not None and agency.text:
                        grant_info["agency"] = agency.text
                    if grant_info:
                        grants.append(grant_info)
                if grants:
                    article_metadata["grants"] = grants

                # Check for free full text in PMC
                pmc_elem = article.find(".//ArticleId[@IdType='pmc']")
                if pmc_elem is not None:
                    article_metadata["has_free_full_text"] = True
                    article_metadata["pmc_id"] = pmc_elem.text

                # Extract conflict of interest statement
                coi_elem = article.find(".//CoiStatement")
                if coi_elem is not None and coi_elem.text:
                    article_metadata["conflict_of_interest"] = coi_elem.text

                metadata[pmid] = article_metadata

            return metadata

        except Exception:
            logger.exception("Error getting detailed article metadata")
            return {}

    def _create_enriched_content(
        self, result: Dict[str, Any], base_content: str
    ) -> str:
        """
        Create enriched content by adding relevant metadata context to help the LLM.

        Args:
            result: The result dictionary with metadata
            base_content: The base content (abstract or full text)

        Returns:
            Enriched content string with metadata context
        """
        enriched_parts = []

        # Add study type information
        if "publication_types" in result:
            pub_types = result["publication_types"]
            # Filter for significant types
            significant_types = [
                pt
                for pt in pub_types
                if any(
                    key in pt.lower()
                    for key in [
                        "clinical trial",
                        "randomized",
                        "meta-analysis",
                        "systematic review",
                        "case report",
                        "guideline",
                        "comparative study",
                        "multicenter",
                    ]
                )
            ]
            if significant_types:
                enriched_parts.append(
                    f"[Study Type: {', '.join(significant_types)}]"
                )

        # Add the main content
        enriched_parts.append(base_content)

        # Add metadata footer
        metadata_footer = []

        # Add ALL MeSH terms
        if "mesh_terms" in result and len(result["mesh_terms"]) > 0:
            metadata_footer.append(
                f"Medical Topics (MeSH): {', '.join(result['mesh_terms'])}"
            )

        # Add ALL keywords
        if "keywords" in result and len(result["keywords"]) > 0:
            metadata_footer.append(f"Keywords: {', '.join(result['keywords'])}")

        # Add ALL affiliations
        if "affiliations" in result and len(result["affiliations"]) > 0:
            if len(result["affiliations"]) == 1:
                metadata_footer.append(
                    f"Institution: {result['affiliations'][0]}"
                )
            else:
                affiliations_text = "\n  - " + "\n  - ".join(
                    result["affiliations"]
                )
                metadata_footer.append(f"Institutions:{affiliations_text}")

        # Add ALL funding information with full details
        if "grants" in result and len(result["grants"]) > 0:
            grant_details = []
            for grant in result["grants"]:
                grant_text = []
                if "agency" in grant:
                    grant_text.append(grant["agency"])
                if "id" in grant:
                    grant_text.append(f"(Grant ID: {grant['id']})")
                if grant_text:
                    grant_details.append(" ".join(grant_text))
            if grant_details:
                if len(grant_details) == 1:
                    metadata_footer.append(f"Funded by: {grant_details[0]}")
                else:
                    funding_text = "\n  - " + "\n  - ".join(grant_details)
                    metadata_footer.append(f"Funding Sources:{funding_text}")

        # Add FULL conflict of interest statement
        if "conflict_of_interest" in result:
            coi_text = result["conflict_of_interest"]
            if coi_text:
                # Still skip trivial "no conflict" statements to reduce noise
                if not any(
                    phrase in coi_text.lower()
                    for phrase in [
                        "no conflict",
                        "no competing",
                        "nothing to disclose",
                        "none declared",
                        "authors declare no",
                    ]
                ):
                    metadata_footer.append(f"Conflict of Interest: {coi_text}")
                elif (
                    "but" in coi_text.lower()
                    or "except" in coi_text.lower()
                    or "however" in coi_text.lower()
                ):
                    # Include if there's a "no conflict BUT..." type statement
                    metadata_footer.append(f"Conflict of Interest: {coi_text}")

        # Combine everything
        if metadata_footer:
            enriched_parts.append("\n---\nStudy Metadata:")
            enriched_parts.extend(metadata_footer)

        return "\n".join(enriched_parts)

    def _find_pmc_ids(self, pmid_list: List[str]) -> Dict[str, str]:
        """
        Find PMC IDs for the given PubMed IDs (for full-text access).

        Args:
            pmid_list: List of PubMed IDs

        Returns:
            Dictionary mapping PubMed IDs to their PMC IDs (if available)
        """
        if not pmid_list or not self.get_full_text:
            return {}

        try:
            # Prepare parameters
            params = {
                "dbfrom": "pubmed",
                "db": "pmc",
                "linkname": "pubmed_pmc",
                "id": ",".join(pmid_list),
                "retmode": "json",
            }

            # Add API key if available
            if self.api_key:
                params["api_key"] = self.api_key

            self._last_wait_time = self.rate_tracker.apply_rate_limit(
                self.engine_type
            )

            # Execute request
            response = safe_get(self.link_url, params=params)
            response.raise_for_status()

            # Parse response
            data = response.json()

            # Map PubMed IDs to PMC IDs
            pmid_to_pmcid = {}

            for linkset in data.get("linksets", []):
                pmid = linkset.get("ids", [None])[0]

                if not pmid:
                    continue

                for link in linkset.get("linksetdbs", []):
                    if link.get("linkname") == "pubmed_pmc":
                        pmcids = link.get("links", [])
                        if pmcids:
                            pmid_to_pmcid[str(pmid)] = f"PMC{pmcids[0]}"

            logger.info(
                f"Found {len(pmid_to_pmcid)} PMC IDs for full-text access"
            )
            return pmid_to_pmcid

        except Exception:
            logger.exception("Error finding PMC IDs")
            return {}

    def _get_pmc_full_text(self, pmcid: str) -> str:
        """
        Get full text for a PMC article.

        Args:
            pmcid: PMC ID of the article

        Returns:
            Full text content or empty string if not available
        """
        try:
            # Prepare parameters
            params = {
                "db": "pmc",
                "id": pmcid,
                "retmode": "xml",
                "rettype": "full",
            }

            # Add API key if available
            if self.api_key:
                params["api_key"] = self.api_key

            self._last_wait_time = self.rate_tracker.apply_rate_limit(
                self.engine_type
            )

            # Execute request
            response = safe_get(self.fetch_url, params=params)
            response.raise_for_status()

            # Parse XML response
            root = ET.fromstring(response.text)

            # Extract full text
            full_text = []

            # Extract article title
            title_elem = root.find(".//article-title")
            if title_elem is not None and title_elem.text:
                full_text.append(f"# {title_elem.text}")

            # Extract abstract
            abstract_paras = root.findall(".//abstract//p")
            if abstract_paras:
                full_text.append("\n## Abstract\n")
                for p in abstract_paras:
                    text = "".join(p.itertext())
                    if text:
                        full_text.append(text)

            # Extract body content
            body = root.find(".//body")
            if body is not None:
                for section in body.findall(".//sec"):
                    # Get section title
                    title = section.find(".//title")
                    if title is not None and title.text:
                        full_text.append(f"\n## {title.text}\n")

                    # Get paragraphs
                    for p in section.findall(".//p"):
                        text = "".join(p.itertext())
                        if text:
                            full_text.append(text)

            result_text = "\n\n".join(full_text)
            logger.debug(
                f"Successfully extracted {len(result_text)} chars of PMC full text with {len(full_text)} sections"
            )
            return result_text

        except Exception:
            logger.exception("Error getting PMC full text")
            return ""

    def _get_previews(self, query: str) -> List[Dict[str, Any]]:
        """
        Get preview information for PubMed articles.

        Args:
            query: The search query

        Returns:
            List of preview dictionaries
        """
        logger.info(f"Getting PubMed previews for query: {query}")

        # Optimize the query for PubMed if LLM is available
        optimized_query = self._optimize_query_for_pubmed(query)

        # Perform adaptive search
        pmid_list, strategy = self._adaptive_search(optimized_query)

        # If no results, try a simplified query
        if not pmid_list:
            logger.warning(
                f"No PubMed results found using strategy: {strategy}"
            )
            simplified_query = self._simplify_query(optimized_query)
            if simplified_query != optimized_query:
                logger.info(f"Trying with simplified query: {simplified_query}")
                pmid_list, strategy = self._adaptive_search(simplified_query)
                if pmid_list:
                    logger.info(
                        f"Simplified query found {len(pmid_list)} results"
                    )

        if not pmid_list:
            logger.warning("No PubMed results found after query simplification")
            return []

        # Get article summaries
        logger.debug(f"Fetching article summaries for {len(pmid_list)} PMIDs")
        summaries = self._get_article_summaries(pmid_list)
        logger.debug(f"Retrieved {len(summaries)} summaries")

        # ALWAYS fetch abstracts for snippet-only mode to provide context for LLM
        logger.debug(
            f"Fetching abstracts for {len(pmid_list)} articles for snippet enrichment"
        )
        abstracts = self._get_article_abstracts(pmid_list)
        logger.debug(f"Retrieved {len(abstracts)} abstracts")

        # Format as previews
        previews = []
        for summary in summaries:
            # Build snippet from individual metadata preferences
            snippet_parts = []

            # Check for publication type from esummary (earlier than detailed metadata)
            pub_type_prefix = ""
            if self.include_publication_type_in_context and summary.get(
                "pubtype"
            ):
                # Use first publication type from esummary
                pub_type_prefix = f"[{summary['pubtype'][0]}] "

            # Add authors if enabled
            if self.include_authors_in_context and summary.get("authors"):
                authors_text = ", ".join(summary.get("authors", []))
                if len(authors_text) > 100:
                    # Truncate long author lists
                    authors_text = authors_text[:97] + "..."
                snippet_parts.append(authors_text)

            # Add journal if enabled
            if self.include_journal_in_context and summary.get("journal"):
                snippet_parts.append(summary["journal"])

            # Add date (full or year only)
            if summary.get("pubdate"):
                if self.include_full_date_in_context:
                    snippet_parts.append(summary["pubdate"])
                elif (
                    self.include_year_in_context
                    and len(summary["pubdate"]) >= 4
                ):
                    snippet_parts.append(summary["pubdate"][:4])

            # Add citation details if enabled
            if self.include_citation_in_context:
                citation_parts = []
                if summary.get("volume"):
                    citation_parts.append(f"Vol {summary['volume']}")
                if summary.get("issue"):
                    citation_parts.append(f"Issue {summary['issue']}")
                if summary.get("pages"):
                    citation_parts.append(f"pp {summary['pages']}")
                if citation_parts:
                    snippet_parts.append(f"({', '.join(citation_parts)})")

            # Join snippet parts or provide default
            if snippet_parts:
                # Use different separators based on what's included
                if self.include_authors_in_context:
                    snippet = ". ".join(
                        snippet_parts
                    )  # Authors need period separator
                else:
                    snippet = " - ".join(
                        snippet_parts
                    )  # Journal and year use dash
            else:
                snippet = "Research article"

            # Add publication type prefix
            snippet = pub_type_prefix + snippet

            # Add language indicator if not English
            if self.include_language_in_context and summary.get("lang"):
                langs = summary["lang"]
                if langs and langs[0] != "eng" and langs[0]:
                    snippet = f"{snippet} [{langs[0].upper()}]"

            # Add identifiers if enabled
            identifier_parts = []
            if self.include_pmid_in_context and summary.get("id"):
                identifier_parts.append(f"PMID: {summary['id']}")
            if self.include_doi_in_context and summary.get("doi"):
                identifier_parts.append(f"DOI: {summary['doi']}")

            if identifier_parts:
                snippet = f"{snippet} | {' | '.join(identifier_parts)}"

            # ALWAYS include title and abstract in snippet for LLM analysis
            pmid = summary["id"]
            title = summary["title"]
            abstract_text = abstracts.get(pmid, "")

            # Truncate abstract if too long
            if len(abstract_text) > 500:
                abstract_text = abstract_text[:497] + "..."

            # Build the enriched snippet with title and abstract
            if abstract_text:
                enriched_snippet = f"Title: {title}\n\nAbstract: {abstract_text}\n\nMetadata: {snippet}"
            else:
                enriched_snippet = f"Title: {title}\n\nMetadata: {snippet}"

            # Log the complete snippet for debugging
            logger.debug(f"Complete snippet for PMID {pmid}:")
            logger.debug(f"  Title: {title[:100]}...")
            logger.debug(f"  Abstract length: {len(abstract_text)} chars")
            logger.debug(f"  Metadata: {snippet}")
            logger.debug(
                f"  Full enriched snippet ({len(enriched_snippet)} chars): {enriched_snippet[:500]}..."
            )

            # Create preview with basic information
            preview = {
                "id": summary["id"],
                "title": summary["title"],
                "link": summary["link"],
                "snippet": enriched_snippet,  # Use enriched snippet with title and abstract
                "authors": summary.get("authors", []),
                "journal": summary.get("journal", ""),
                "pubdate": summary.get("pubdate", ""),
                "doi": summary.get("doi", ""),
                "source": "PubMed",
                "_pmid": summary["id"],  # Store PMID for later use
                "_search_strategy": strategy,  # Store search strategy for analytics
            }

            previews.append(preview)

        logger.info(
            f"Found {len(previews)} PubMed previews using strategy: {strategy}"
        )
        if previews:
            logger.debug(
                f"Sample preview title: '{previews[0].get('title', 'NO TITLE')[:80]}...'"
            )
        return previews

    def _get_full_content(
        self, relevant_items: List[Dict[str, Any]]
    ) -> List[Dict[str, Any]]:
        """
        Get full content for the relevant PubMed articles.
        Efficiently manages which content to retrieve (abstracts and/or full text).

        Args:
            relevant_items: List of relevant preview dictionaries

        Returns:
            List of result dictionaries with full content
        """
        # Check if we should add full content
        snippets_only_mode = (
            hasattr(search_config, "SEARCH_SNIPPETS_ONLY")
            and search_config.SEARCH_SNIPPETS_ONLY
        )

        if snippets_only_mode:
            logger.info(
                "Snippet-only mode enabled, will fetch abstracts as snippets"
            )
            # For PubMed, we still need to fetch abstracts as they serve as snippets
            # But we'll skip full-text retrieval

        logger.info(
            f"Getting content for {len(relevant_items)} PubMed articles"
        )

        # Collect all PMIDs for relevant items
        pmids = []
        for item in relevant_items:
            if "_pmid" in item:
                pmids.append(item["_pmid"])

        # Get abstracts if requested and PMIDs exist
        # In snippet-only mode, always get abstracts as they serve as snippets
        abstracts = {}
        if (self.get_abstracts or snippets_only_mode) and pmids:
            abstracts = self._get_article_abstracts(pmids)

        # Get detailed metadata for all articles (publication types, MeSH terms, etc.)
        detailed_metadata = {}
        if pmids:
            detailed_metadata = self._get_article_detailed_metadata(pmids)

        # Find PMC IDs for full-text retrieval (if enabled and not in snippet-only mode)
        pmid_to_pmcid = {}
        if self.get_full_text and pmids and not snippets_only_mode:
            pmid_to_pmcid = self._find_pmc_ids(pmids)

        # Add content to results
        results = []
        for item in relevant_items:
            result = item.copy()
            pmid = item.get("_pmid", "")

            # Add detailed metadata if available
            if pmid in detailed_metadata:
                metadata = detailed_metadata[pmid]

                # Add publication types (e.g., "Clinical Trial", "Meta-Analysis")
                if "publication_types" in metadata:
                    result["publication_types"] = metadata["publication_types"]

                    # Add first publication type to snippet if enabled
                    if (
                        self.include_publication_type_in_context
                        and metadata["publication_types"]
                    ):
                        # Just take the first publication type as is
                        pub_type = metadata["publication_types"][0]
                        if "snippet" in result:
                            result["snippet"] = (
                                f"[{pub_type}] {result['snippet']}"
                            )

                # Add MeSH terms for medical categorization
                if "mesh_terms" in metadata:
                    result["mesh_terms"] = metadata["mesh_terms"]

                    # Add MeSH terms to snippet if enabled
                    if (
                        self.include_mesh_terms_in_context
                        and metadata["mesh_terms"]
                    ):
                        mesh_to_show = (
                            metadata["mesh_terms"][: self.max_mesh_terms]
                            if self.max_mesh_terms > 0
                            else metadata["mesh_terms"]
                        )
                        if mesh_to_show and "snippet" in result:
                            mesh_text = "MeSH: " + ", ".join(mesh_to_show)
                            result["snippet"] = (
                                f"{result['snippet']} | {mesh_text}"
                            )

                # Add keywords
                if "keywords" in metadata:
                    result["keywords"] = metadata["keywords"]

                    # Add keywords to snippet if enabled
                    if (
                        self.include_keywords_in_context
                        and metadata["keywords"]
                    ):
                        keywords_to_show = (
                            metadata["keywords"][: self.max_keywords]
                            if self.max_keywords > 0
                            else metadata["keywords"]
                        )
                        if keywords_to_show and "snippet" in result:
                            keywords_text = "Keywords: " + ", ".join(
                                keywords_to_show
                            )
                            result["snippet"] = (
                                f"{result['snippet']} | {keywords_text}"
                            )

                # Add affiliations
                if "affiliations" in metadata:
                    result["affiliations"] = metadata["affiliations"]

                # Add funding/grant information
                if "grants" in metadata:
                    result["grants"] = metadata["grants"]

                # Add conflict of interest statement
                if "conflict_of_interest" in metadata:
                    result["conflict_of_interest"] = metadata[
                        "conflict_of_interest"
                    ]

                # Add free full text availability
                if "has_free_full_text" in metadata:
                    result["has_free_full_text"] = metadata[
                        "has_free_full_text"
                    ]
                    if "pmc_id" in metadata:
                        result["pmc_id"] = metadata["pmc_id"]

                    # Add PMC availability to snippet if enabled
                    if (
                        self.include_pmc_availability_in_context
                        and metadata["has_free_full_text"]
                        and "snippet" in result
                    ):
                        result["snippet"] = (
                            f"{result['snippet']} | [Free Full Text]"
                        )

            # Add abstract if available
            if pmid in abstracts:
                result["abstract"] = abstracts[pmid]

                # Create enriched content with metadata context
                enriched_content = self._create_enriched_content(
                    result, abstracts[pmid]
                )

                # ALWAYS include title and abstract in snippet for LLM analysis
                # Build comprehensive snippet with title and abstract
                title = result.get("title", "")
                abstract_text = (
                    abstracts[pmid][:500]
                    if len(abstracts[pmid]) > 500
                    else abstracts[pmid]
                )

                # Prepend title and abstract to the existing metadata snippet
                if "snippet" in result:
                    # Keep metadata snippet and add content
                    result["snippet"] = (
                        f"Title: {title}\n\nAbstract: {abstract_text}\n\nMetadata: {result['snippet']}"
                    )
                else:
                    # No metadata snippet, just title and abstract
                    result["snippet"] = (
                        f"Title: {title}\n\nAbstract: {abstract_text}"
                    )

                # In snippet-only mode, use enriched content
                if snippets_only_mode:
                    result["full_content"] = enriched_content
                    result["content"] = enriched_content
                    result["content_type"] = "abstract"
                # Use abstract as content if no full text
                elif pmid not in pmid_to_pmcid:
                    result["full_content"] = enriched_content
                    result["content"] = enriched_content
                    result["content_type"] = "abstract"

            # Add full text for a limited number of top articles
            if (
                pmid in pmid_to_pmcid
                and self.get_full_text
                and len(
                    [r for r in results if r.get("content_type") == "full_text"]
                )
                < self.full_text_limit
            ):
                # Get full text content
                pmcid = pmid_to_pmcid[pmid]
                full_text = self._get_pmc_full_text(pmcid)

                if full_text:
                    enriched_full_text = self._create_enriched_content(
                        result, full_text
                    )
                    result["full_content"] = enriched_full_text
                    result["content"] = enriched_full_text
                    result["content_type"] = "full_text"
                    result["pmcid"] = pmcid
                elif pmid in abstracts:
                    # Fall back to abstract if full text retrieval fails
                    enriched_content = self._create_enriched_content(
                        result, abstracts[pmid]
                    )
                    result["full_content"] = enriched_content
                    result["content"] = enriched_content
                    result["content_type"] = "abstract"

            # Remove temporary fields
            if "_pmid" in result:
                del result["_pmid"]
            if "_search_strategy" in result:
                del result["_search_strategy"]

            results.append(result)

        return results

    def search_by_author(
        self, author_name: str, max_results: Optional[int] = None
    ) -> List[Dict[str, Any]]:
        """
        Search for articles by a specific author.

        Args:
            author_name: Name of the author
            max_results: Maximum number of results (defaults to self.max_results)

        Returns:
            List of articles by the author
        """
        original_max_results = self.max_results

        try:
            if max_results:
                self.max_results = max_results

            query = f"{author_name}[Author]"
            return self.run(query)

        finally:
            # Restore original value
            self.max_results = original_max_results

    def search_by_journal(
        self, journal_name: str, max_results: Optional[int] = None
    ) -> List[Dict[str, Any]]:
        """
        Search for articles in a specific journal.

        Args:
            journal_name: Name of the journal
            max_results: Maximum number of results (defaults to self.max_results)

        Returns:
            List of articles from the journal
        """
        original_max_results = self.max_results

        try:
            if max_results:
                self.max_results = max_results

            query = f"{journal_name}[Journal]"
            return self.run(query)

        finally:
            # Restore original value
            self.max_results = original_max_results

    def search_recent(
        self, query: str, days: int = 30, max_results: Optional[int] = None
    ) -> List[Dict[str, Any]]:
        """
        Search for recent articles matching the query.

        Args:
            query: The search query
            days: Number of days to look back
            max_results: Maximum number of results (defaults to self.max_results)

        Returns:
            List of recent articles matching the query
        """
        original_max_results = self.max_results
        original_days_limit = self.days_limit

        try:
            if max_results:
                self.max_results = max_results

            # Set days limit for this search
            self.days_limit = days

            return self.run(query)

        finally:
            # Restore original values
            self.max_results = original_max_results
            self.days_limit = original_days_limit

    def advanced_search(
        self, terms: Dict[str, str], max_results: Optional[int] = None
    ) -> List[Dict[str, Any]]:
        """
        Perform an advanced search with field-specific terms.

        Args:
            terms: Dictionary mapping fields to search terms
                  Valid fields: Author, Journal, Title, MeSH, Affiliation, etc.
            max_results: Maximum number of results (defaults to self.max_results)

        Returns:
            List of articles matching the advanced query
        """
        original_max_results = self.max_results

        try:
            if max_results:
                self.max_results = max_results

            # Build advanced query string
            query_parts = []
            for field, term in terms.items():
                query_parts.append(f"{term}[{field}]")

            query = " AND ".join(query_parts)
            return self.run(query)

        finally:
            # Restore original value
            self.max_results = original_max_results
