# app.py
import os
import logging
import asyncio
import nest_asyncio
from datetime import datetime
import uuid
import aiohttp
import gradio as gr

from langfuse.llama_index import LlamaIndexInstrumentor
from llama_index.tools.duckduckgo import DuckDuckGoSearchToolSpec
from llama_index.tools.weather import OpenWeatherMapToolSpec
from llama_index.tools.playwright import PlaywrightToolSpec
from llama_index.core.tools import FunctionTool
from llama_index.core.agent.workflow import AgentWorkflow
from llama_index.core.workflow import Context
from llama_index.llms.huggingface_api import HuggingFaceInferenceAPI
from llama_index.core.memory import ChatMemoryBuffer
from llama_index.readers.web import RssReader

import subprocess
subprocess.run(["playwright", "install"])

# allow nested loops in Spaces
nest_asyncio.apply()

# --- Llangfuse ---
instrumentor = LlamaIndexInstrumentor(
    public_key=os.environ.get("LANGFUSE_PUBLIC_KEY"),
    secret_key=os.environ.get("LANGFUSE_SECRET_KEY"),
    host=os.environ.get("LANGFUSE_HOST"),
)
instrumentor.start()

# --- Secrets via env vars ---
HF_TOKEN            = os.getenv("HF_TOKEN")
# OPENAI_API_KEY      = os.getenv("OPENAI_API_KEY")
OPENWEATHERMAP_KEY  = os.getenv("OPENWEATHERMAP_API_KEY")
SERPER_API_KEY      = os.getenv("SERPER_API_KEY")

# --- LLMs ---
llm = HuggingFaceInferenceAPI(
    model_name="Qwen/Qwen2.5-Coder-32B-Instruct",
    token=HF_TOKEN, 
    task="conversational"
)

memory = ChatMemoryBuffer.from_defaults(token_limit=8192)
today_str = datetime.now().strftime("%B %d, %Y")
ANON_USER_ID = os.environ.get("ANON_USER_ID", uuid.uuid4().hex)

# # OpenAI for pure function-calling
# openai_llm = OpenAI(
#     model="gpt-4o",
#     api_key=OPENAI_API_KEY,
#     temperature=0.0,
#     streaming=False,
# )

# --- Tools Setup ---
# DuckDuckGo
duck_spec = DuckDuckGoSearchToolSpec()
search_tool = FunctionTool.from_defaults(duck_spec.duckduckgo_full_search)

# Weather
openweather_api_key=OPENWEATHERMAP_KEY
weather_tool_spec = OpenWeatherMapToolSpec(key=openweather_api_key)
weather_tool_spec = OpenWeatherMapToolSpec(key=openweather_api_key)
weather_tool = FunctionTool.from_defaults(
    weather_tool_spec.weather_at_location,
    name="current_weather",
    description="Get the current weather at a specific location (city, country)."
)
forecast_tool = FunctionTool.from_defaults(
    weather_tool_spec.forecast_tommorrow_at_location,
    name="weather_forecast",
    description="Get tomorrow's weather forecast for a specific location (city, country)."
)

# Playwright (synchronous start)
async def _start_browser():
    return await PlaywrightToolSpec.create_async_playwright_browser(headless=True)
browser = asyncio.get_event_loop().run_until_complete(_start_browser())
playwright_tool_spec = PlaywrightToolSpec.from_async_browser(browser)

navigate_tool = FunctionTool.from_defaults(
    playwright_tool_spec.navigate_to,
    name="web_navigate",
    description="Navigate to a specific URL."
)
extract_text_tool = FunctionTool.from_defaults(
    playwright_tool_spec.extract_text,
    name="web_extract_text",
    description="Extract all text from the current page."
)
extract_links_tool = FunctionTool.from_defaults(
    playwright_tool_spec.extract_hyperlinks,
    name="web_extract_links",
    description="Extract all hyperlinks from the current page."
)

# Google News RSS
def fetch_google_news_rss():
    docs = RssReader(html_to_text=True).load_data(["https://news.google.com/rss"])
    return [{"title":d.metadata.get("title",""), "url":d.metadata.get("link","")} for d in docs]
google_rss_tool = FunctionTool.from_defaults(
    fn=fetch_google_news_rss,
    name="fetch_google_news_rss",
    description="Fetch latest headlines and URLs from Google News RSS."
)

# Serper
async def fetch_serper_news(query: str):
    if not serper_api_key:
        raise ValueError("Missing SERPER_API_KEY environment variable")
    url = f"https://google.serper.dev/news?q={query}&tbs=qdr%3Ad"
    headers = {"X-API-KEY": serper_api_key, "Content-Type": "application/json"}
    async with aiohttp.ClientSession() as session:
        async with session.get(url, headers=headers) as resp:
            resp.raise_for_status()
            return await resp.json()

serper_news_tool = FunctionTool.from_defaults(
    fetch_serper_news,
    name="fetch_news_from_serper",
    description="Fetch news articles on a given topic via the Serper API."
)

# Create the agent workflow
tools = [
    duckduckgo_tool,
    navigate_tool,
    extract_text_tool,
    extract_links_tool,
    weather_tool,
    forecast_tool,
    google_rss_tool,
    serper_news_tool,
]
web_agent = AgentWorkflow.from_tools_or_functions(tools, llm=llm)
ctx = Context(web_agent)

# Async helper to run agent queries
def run_query_sync(query: str):
    """Helper to run async agent.run in sync context."""
    return asyncio.get_event_loop().run_until_complete(
        web_agent.run(query, ctx=ctx)
    )

async def run_query(query: str):
    trace_id = f"agent-run-{uuid.uuid4().hex}"
    try:
        with instrumentor.observe(
            trace_id=trace_id,
            session_id="web-agent-session",
            user_id=ANON_USER_ID,
        ):
            return await web_agent.run(query, ctx=ctx)
    finally:
        instrumentor.flush()

# Gradio interface function
async def gradio_query(user_input, chat_history=None):
    chat_history = chat_history or []
    result = await run_query(user_input)
    response = result.response
    chat_history.append((user_input, response))
    return chat_history, chat_history

# Build and launch Gradio app
grb = gr.Blocks()
with grb:
    gr.Markdown("## Perspicacity")
    gr.Markdown(
        "This bot can check the news, tell you the weather, and even browse websites to answer follow-up questions β€” all powered by a team of tiny AI agents working behind the scenes.\n\n"
        "πŸ§ͺ Built for fun during the [AI Agents course](https://huggingface.co/learn/agents-course/unit0/introduction) β€” it's just a demo to show what agents can do.  \n"
        "πŸ™Œ Got ideas or improvements? PRs welcome!  \n\n"
        "πŸ‘‰ _Try asking β€œWhat’s the weather in Montreal?” or β€œWhat’s in the news today?”_"
    )
    chatbot = gr.Chatbot()  # conversation UI
    txt = gr.Textbox(placeholder="Ask me anything...", show_label=False)
    txt.submit(gradio_query, [txt, chatbot], [chatbot, chatbot])
    gr.Button("Send").click(gradio_query, [txt, chatbot], [chatbot, chatbot])

if __name__ == "__main__":
    grb.launch()