# Web research function for LLM-powered web search
# Precision-focused: prioritizes accuracy over quantity

def web_research_new(
    search_question,
    search_input,
    api_key,
    answer_format="concise",
    additional_instructions="",
    categories=['Answer'],
    user_model="claude-sonnet-4-20250514",
    creativity=None,
    safety=False,
    filename="web_research_results.csv",
    save_directory=None,
    model_source="anthropic",
    start_date=None,
    end_date=None,
    output_urls=True,
    max_retries=6,
    time_delay=5
):
    """
    Perform precision-focused web research using LLMs.

    This function searches the web for information and returns structured responses
    with confidence scores. It prioritizes accuracy over quantity - returning
    'Information not found' rather than uncertain answers.

    Args:
        search_question (str): The question to answer for each search item.
            Example: "current CEO" or "founding date"
        search_input (list or pd.Series): List of items to search for.
            Example: ["Apple Inc", "Microsoft", "Google"]
        api_key (str): API key for the model provider.
        answer_format (str): How to format answers. Default "concise".
        additional_instructions (str): Extra instructions for the search.
        categories (list): Categories for responses. Default ['Answer'].
        user_model (str): Model to use. Default "claude-sonnet-4-20250514".
        creativity (float): Temperature setting. None uses model default.
        safety (bool): If True, saves progress after each item.
        filename (str): Output filename. Default "web_research_results.csv".
        save_directory (str): Directory to save results. None uses current dir.
        model_source (str): Provider - "anthropic", "google", or "perplexity".
        start_date (str): Filter results after this date (YYYY-MM-DD).
        end_date (str): Filter results before this date (YYYY-MM-DD).
        output_urls (bool): Include source URLs in output. Default True.
        max_retries (int): Retry count for rate limit errors. Default 6.
        time_delay (int): Seconds between requests. Default 5.

    Returns:
        pd.DataFrame: Results with columns:
            - search_input: Original search item
            - raw_response: Full model response (basic search only)
            - answer: Extracted answer or "Information not found"
            - confidence: Confidence score 0-5
            - url_0, url_1, ...: Source URLs (if output_urls=True)

    Example:
        >>> import llm_web_research as lwr
        >>> results = lwr.web_research(
        ...     search_question="current CEO",
        ...     search_input=["Apple Inc", "Microsoft"],
        ...     api_key="your-api-key",
        ...     model_source="anthropic"
        ... )
    """
    import os
    import re
    import json
    import pandas as pd
    import regex
    from tqdm import tqdm
    import time
    from datetime import datetime

    def _validate_date(date_str):
        """Validates YYYY-MM-DD format"""
        if date_str is None:
            return True

        if not isinstance(date_str, str):
            return False

        pattern = r'^\d{4}-\d{2}-\d{2}$'
        if not re.match(pattern, date_str):
            return False

        try:
            year, month, day = date_str.split('-')
            datetime(int(year), int(month), int(day))
            return True
        except (ValueError, OverflowError):
            return False

    if not _validate_date(start_date):
        raise ValueError(f"start_date must be in YYYY-MM-DD format, got: {start_date}")

    if not _validate_date(end_date):
        raise ValueError(f"end_date must be in YYYY-MM-DD format, got: {end_date}")

    model_source = model_source.lower()

    if model_source == "perplexity" and start_date is not None:
        start_date = datetime.strptime(start_date, "%Y-%m-%d").strftime("%m/%d/%Y")
    if model_source == "perplexity" and end_date is not None:
        end_date = datetime.strptime(end_date, "%Y-%m-%d").strftime("%m/%d/%Y")

    if model_source == "google" and user_model == "claude-sonnet-4-20250514":
        user_model = "gemini-2.5-flash"

    categories_str = "\n".join(f"{i + 1}. {cat}" for i, cat in enumerate(categories))
    cat_num = len(categories)
    category_dict = {str(i+1): "0" for i in range(cat_num)}
    example_JSON = json.dumps(category_dict, indent=4)

    link1 = []
    extracted_jsons = []
    extracted_urls = []

    for idx, item in enumerate(tqdm(search_input, desc="Searching")):
        if idx > 0:
            time.sleep(time_delay)
        reply = None

        if pd.isna(item):
            link1.append("Skipped NaN input")
            extracted_urls.append([])
            default_json = example_JSON
            extracted_jsons.append(default_json)
        else:
            prompt = f"""<role>You are a research assistant specializing in finding current, factual information.</role>

            <task>Find information about {item}'s {search_question}</task>

            <rules>
            - Search for the most current and authoritative information available
            - Provide your answer as {answer_format}
            - Prioritize official sources when possible
            - If information is not found, state "Information not found"
            - Do not include any explanatory text or commentary beyond the JSON
                {additional_instructions}
            </rules>

            <format>
            Return your response as valid JSON with this exact structure:
            {{
            "answer": "Your factual answer or 'Information not found'",
            "second_best_answer": "Your second best factual answer or 'Information not found'",
            "confidence": "confidence in response 0-5 or 'Information not found'"
        }}

        </format>"""

            if start_date is not None and end_date is not None:
                append_text = f"\n- Focus on webpages with a page age between {start_date} and {end_date}."
                prompt = prompt.replace("<rules>", "<rules>" + append_text)
            elif start_date is not None:
                append_text = f"\n- Focus on webpages published after {start_date}."
                prompt = prompt.replace("<rules>", "<rules>" + append_text)
            elif end_date is not None:
                append_text = f"\n- Focus on webpages published before {end_date}."
                prompt = prompt.replace("<rules>", "<rules>" + append_text)

            if model_source == "anthropic":
                import anthropic
                client = anthropic.Anthropic(api_key=api_key)
                attempt = 0
                while attempt < max_retries:
                    try:
                        message = client.messages.create(
                        model=user_model,
                        max_tokens=1024,
                        messages=[{"role": "user", "content": prompt}],
                        **({"temperature": creativity} if creativity is not None else {}),
                        tools=[{
                        "type": "web_search_20250305",
                        "name": "web_search"
                        }]
                    )
                        reply = " ".join(
                            block.text
                            for block in message.content
                            if getattr(block, "type", "") == "text"
                        ).strip()
                        link1.append(reply)

                        urls = [
                            item["url"]
                            for block in message.content
                            if getattr(block, "type", "") == "web_search_tool_result"
                            for item in (getattr(block, "content", []) or [])
                            if isinstance(item, dict) and item.get("type") == "web_search_result" and "url" in item
                        ]

                        seen = set()
                        urls = [u for u in urls if not (u in seen or seen.add(u))]
                        extracted_urls.append(urls)

                        break
                    except anthropic.RateLimitError as e:
                        wait_time = 2 ** attempt
                        time.sleep(wait_time)
                        attempt += 1
                    except Exception as e:
                        link1.append(f"Error processing input: {e}")
                        extracted_urls.append([])
                        break
                else:
                    link1.append("Max retries exceeded for rate limit errors.")
                    extracted_urls.append([])

            elif model_source == "google":
                import requests
                url = f"https://generativelanguage.googleapis.com/v1beta/models/{user_model}:generateContent"
                try:
                    headers = {
                        "x-goog-api-key": api_key,
                        "Content-Type": "application/json"
                    }
                    payload = {
                        "contents": [{"parts": [{"text": prompt}]}],
                        "tools": [{"google_search": {}}],
                        **({"generationConfig": {"temperature": creativity}} if creativity is not None else {})
                    }

                    response = requests.post(url, headers=headers, json=payload)
                    response.raise_for_status()
                    result = response.json()

                    urls = []
                    for cand in result.get("candidates", []):
                        rendered_html = (
                            cand.get("groundingMetadata", {})
                            .get("searchEntryPoint", {})
                            .get("renderedContent", "")
                        )
                        if rendered_html:
                            found = re.findall(
                                r'<a[^>]*class=["\']chip["\'][^>]*href=["\']([^"\']+)["\']',
                                rendered_html,
                                flags=re.IGNORECASE
                            )
                            urls.extend(found)

                    seen = set()
                    urls = [u for u in urls if not (u in seen or seen.add(u))]
                    extracted_urls.append(urls)

                    if "candidates" in result and result["candidates"]:
                        reply = result["candidates"][0]["content"]["parts"][0]["text"]
                    else:
                        reply = "No response generated"

                    link1.append(reply)

                except Exception as e:
                    link1.append(f"Error processing input: {e}")
                    extracted_urls.append([])

            elif model_source == "perplexity":

                from perplexity import Perplexity
                client = Perplexity(api_key=api_key)
                try:
                    response = client.chat.completions.create(
                        messages=[
                            {
                                "role": "user",
                                "content": prompt
                            }
                        ],
                        model=user_model,
                        max_tokens=1024,
                        **({"temperature": creativity} if creativity is not None else {}),
                        web_search_options={"search_context_size": "medium"},
                        **({"search_after_date_filter": start_date} if start_date else {}),
                        **({"search_before_date_filter": end_date} if end_date else {}),
                        response_format={
                        "type": "json_schema",
                        "json_schema": {
                            "schema": {
                                "type": "object",
                                "properties": {
                                    "answer": {"type": "string"},
                                    "second_best_answer": {"type": "string"},
                                    "confidence": {"type": "integer"}
                            },
                            "required": ["answer", "second_best_answer"]
                        }
                    }
                }
            )

                    reply = response.choices[0].message.content
                    link1.append(reply)

                    urls = list(response.citations) if hasattr(response, 'citations') else []

                    seen = set()
                    urls = [u for u in urls if not (u in seen or seen.add(u))]
                    extracted_urls.append(urls)

                except Exception as e:
                    link1.append(f"Error processing input: {e}")
                    extracted_urls.append([])
            else:
                raise ValueError("Unknown source! Currently this function only supports 'anthropic', 'google', or 'perplexity' as model_source.")

            if reply is not None:
                extracted_json = regex.findall(r'\{(?:[^{}]|(?R))*\}', reply, regex.DOTALL)
                if extracted_json:
                    raw_json = extracted_json[0].strip()
                    try:
                        parsed_obj = json.loads(raw_json)
                        cleaned_json = json.dumps(parsed_obj)
                        extracted_jsons.append(cleaned_json)
                    except json.JSONDecodeError as e:
                        extracted_jsons.append(raw_json)
                else:
                    error_message = json.dumps({"answer": "e"})
                    extracted_jsons.append(error_message)
            else:
                error_message = json.dumps({"answer": "e"})
                extracted_jsons.append(error_message)

        if safety:
            temp_df = pd.DataFrame({
                'raw_response': search_input[:idx+1]
            })
            normalized_data_list = []
            for json_str in extracted_jsons:
                try:
                    parsed_obj = json.loads(json_str)
                    normalized_data_list.append(pd.json_normalize(parsed_obj))
                except json.JSONDecodeError:
                    normalized_data_list.append(pd.DataFrame({"answer": ["e"]}))
            normalized_data = pd.concat(normalized_data_list, ignore_index=True)
            temp_urls = pd.DataFrame(extracted_urls).add_prefix("url_")
            temp_df = pd.concat([temp_df, normalized_data], axis=1)
            temp_df = pd.concat([temp_df, temp_urls], axis=1)
            if save_directory is None:
                save_directory = os.getcwd()
            temp_df.to_csv(os.path.join(save_directory, filename), index=False)

    normalized_data_list = []
    for json_str in extracted_jsons:
        try:
            parsed_obj = json.loads(json_str)
            normalized_data_list.append(pd.json_normalize(parsed_obj))
        except json.JSONDecodeError:
            normalized_data_list.append(pd.DataFrame({"answer": ["e"]}))
    normalized_data = pd.concat(normalized_data_list, ignore_index=True)

    df_urls = pd.DataFrame(extracted_urls).add_prefix("url_")

    categorized_data = pd.DataFrame({
        'search_input': (
            search_input.reset_index(drop=True) if isinstance(search_input, (pd.DataFrame, pd.Series))
            else pd.Series(search_input)
        ),
        'raw_response': pd.Series(link1).reset_index(drop=True),
    })
    categorized_data = pd.concat([categorized_data, normalized_data], axis=1)
    categorized_data = pd.concat([categorized_data, df_urls], axis=1)

    categorized_data = categorized_data.drop(columns=["second_best_answer"], errors='ignore')

    if output_urls is False:
        categorized_data = categorized_data.drop(columns=[col for col in categorized_data.columns if col.startswith("url_")])

    if save_directory is not None:
        categorized_data.to_csv(os.path.join(save_directory, filename), index=False)

    return categorized_data
