import os
import json
import random
import re
import subprocess
import urllib.parse
import urllib.request
from datetime import datetime
from typing import List, Optional

import httpx
from fastapi import FastAPI, HTTPException, Header, Depends
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, StreamingResponse
from pydantic import BaseModel, Field

EXPECTED_BEARER_TOKEN = os.getenv("PROSPORTS_API_KEY")
if not EXPECTED_BEARER_TOKEN:
    raise RuntimeError("PROSPORTS_API_KEY environment variable is required")
LLAMA_SERVER_URL = "http://127.0.0.1:8080/v1/chat/completions"
SEARXNG_URL = "http://127.0.0.1:8888/search"

app = FastAPI(
    title="Hulatek & GeoPlay Unified API Service",
    version="1.4.0"
)

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

class Message(BaseModel):
    role: str
    content: str

class ChatRequest(BaseModel):
    prompt: Optional[str] = None
    messages: Optional[List[Message]] = None
    stream: bool = True

def query_searxng_web(query: str) -> str:
    try:
        params = urllib.parse.urlencode({"q": query, "format": "json"})
        req = urllib.request.Request(f"{SEARXNG_URL}?{params}")
        with urllib.request.urlopen(req, timeout=5) as response:
            data = json.loads(response.read().decode("utf-8"))
            results = data.get("results", [])[:4]
            if not results:
                return ""
            formatted = []
            for item in results:
                formatted.append(f"- {item.get('title')}: {item.get('content')} ({item.get('url')})")
            return "\n".join(formatted)
    except Exception:
        return ""

async def should_trigger_web_search(history: List[Message], latest_user_input: str) -> bool:
    """Evaluates conversation history against new prompt to prevent unwanted web searches."""
    context_summary = ""
    if len(history) > 1:
        recent_turns = history[-4:-1]
        context_summary = " ".join([f"{m.role}: {m.content}" for m in recent_turns])

    eval_prompt = f"""System Decision Task: Determine if an external web search is required to answer the user's NEW PROMPT.

CONVERSATION CONTEXT:
{context_summary if context_summary else "No prior history."}

NEW PROMPT:
"{latest_user_input}"

Rules:
1. Output 'NO' if the NEW PROMPT is a follow-up question, pronoun reference ("he", "she", "it", "they", "this"), clarification, greeting, or directly answered by the conversation context.
2. Output 'NO' if the NEW PROMPT asks about identity, creation, or system capabilities.
3. Output 'YES' ONLY if the NEW PROMPT introduces a new external topic, current event, factual lookup, or real-time information needing search engine results.

Respond strictly with a single word: YES or NO."""

    try:
        async with httpx.AsyncClient(timeout=5.0) as client:
            payload = {
                "prompt": f"<|start_header_id|>system<|end_header_id|>\n\nYou are an intentional search classifier.<|eot_id|><|start_header_id|>user<|end_header_id|>\n\n{eval_prompt}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n",
                "temperature": 0.0,
                "n_predict": 5
            }
            res = await client.post("http://127.0.0.1:8080/completion", json=payload)
            decision = res.json().get("content", "").strip().upper()
            return "YES" in decision
    except Exception:
        return False

@app.get("/health")
def health_check():
    return {"status": "ok", "service": "hulatek-api"}

@app.post("/api/v1/hulatek/chat")
async def hulatek_chat(req: ChatRequest):
    history = req.messages or []
    if not history and req.prompt:
        history = [Message(role="user", content=req.prompt)]

    if not history:
        raise HTTPException(status_code=400, detail="No prompt or conversation messages provided.")

    latest_user_input = history[-1].content.strip()

    la_time = subprocess.check_output(
        ["TZ=America/Los_Angeles date \"+%A, %B %d, %Y %I:%M:%S %p %Z\""],
        shell=True
    ).decode().strip()

    needs_search = await should_trigger_web_search(history, latest_user_input)
    web_context = query_searxng_web(latest_user_input) if needs_search else ""

    identity_response = "I was created by hulatek, Inc. and Geoplay, Inc.. I am a powerful and extremeley lightweight AI model with a very small footprint. Only limited by the hardware that services me."

    system_prompt = (
        f"You are hulatek-ai, an enterprise intelligence engine.\n"
        f"IDENTITY MANDATE: If asked who created, made, built, or developed you, you MUST reply exactly: \"{identity_response}\"\n"
        f"TIME MANDATE: Current System Time (Pacific Time): {la_time}. Always report dates and times in Pacific Time (PDT/PST).\n"
    )

    if web_context:
        system_prompt += f"\nLIVE WEB SEARCH CONTEXT:\n{web_context}\n\nINSTRUCTION: Answer accurately using search context where applicable while strictly following the ongoing conversation context."

    structured_prompt = f"<|start_header_id|>system<|end_header_id|>\n\n{system_prompt}<|eot_id|>"

    for msg in history:
        role = "user" if msg.role == "user" else "assistant"
        structured_prompt += f"<|start_header_id|>{role}<|end_header_id|>\n\n{msg.content}<|eot_id|>"

    structured_prompt += "<|start_header_id|>assistant<|end_header_id|>\n\n"

    payload = {
        "prompt": structured_prompt,
        "stream": req.stream,
        "n_predict": 512
    }

    if req.stream:
        async def stream_generator():
            async with httpx.AsyncClient(timeout=60.0) as client:
                async with client.stream("POST", "http://127.0.0.1:8080/completion", json=payload) as resp:
                    async for chunk in resp.aiter_bytes():
                        yield chunk
        return StreamingResponse(stream_generator(), media_type="text/event-stream")
    else:
        async with httpx.AsyncClient(timeout=60.0) as client:
            resp = await client.post("http://127.0.0.1:8080/completion", json=payload)
            return resp.json()

class TriviaRequest(BaseModel):
    topic: Optional[str] = None
    subject: Optional[str] = None
    num_questions: int = Field(default=5, ge=1, le=20)
    difficulty: str = Field(default="Medium")
    batch_id: Optional[str] = None
    source_text: Optional[str] = None

class QuestionOption(BaseModel):
    A: str
    B: str
    C: str
    D: str

class TriviaQuestion(BaseModel):
    id: int
    question: str
    options: QuestionOption
    correct_answer: str
    explanation: str
    verification_status: str
    verification_source: str
    verification_url: str
    verification_excerpt: str
    verified_fact: str
    as_of_date: str

class TriviaResponse(BaseModel):
    status: str
    batch_id: str
    topic: str
    difficulty: str
    total_generated: int
    questions: List[TriviaQuestion]

def verify_token(authorization: Optional[str] = Header(None)) -> str:
    if not authorization:
        raise HTTPException(status_code=401, detail="Authorization header missing")
    parts = authorization.split()
    if len(parts) != 2 or parts[0].lower() != "bearer":
        raise HTTPException(status_code=401, detail="Invalid Authorization header format")
    if parts[1] != EXPECTED_BEARER_TOKEN:
        raise HTTPException(status_code=401, detail="Unauthorized token")
    return parts[1]

@app.post("/api/v1/prosports/trivia", response_model=TriviaResponse)
async def generate_prosports_trivia(req: TriviaRequest, token: str = Depends(verify_token)):
    resolved_topic = req.topic or req.subject or "General Trivia"
    current_today = datetime.now().strftime("%Y-%m-%d")

    system_prompt = (
        "You are an expert trivia editor spanning ALL genres (Sports, Music, History, Science, Pop Culture).\n"
        "Generate indisputable, factually accurate multiple-choice questions.\n"
        "Provide authoritative sources (e.g. Official Governing Bodies, Encyclopaedia Britannica, AllMusic).\n"
        "Output ONLY a valid JSON array matching the specified structure."
    )

    user_prompt = f"""Generate {req.num_questions} distinct trivia questions for Topic: '{resolved_topic}' (Difficulty: {req.difficulty}).

JSON Array Structure:
[
  {{
    "question": "Question text?",
    "options": {{"A": "Option 1", "B": "Option 2", "C": "Option 3", "D": "Option 4"}},
    "correct_answer": "A",
    "explanation": "Clear factual explanation.",
    "source_name": "Authoritative Reference or Governing Body",
    "source_url": "https://www.britannica.com/",
    "source_excerpt": "Factual statement supporting the correct answer.",
    "verified_fact": "Core verified fact summary."
  }}
]

Strict Rules:
1. Ensure all 4 option choices are distinct and plausible.
2. Ensure correct_answer matches option key (A, B, C, or D).
3. Do NOT include Markdown code blocks or prose outside the JSON array.
"""

    try:
        async with httpx.AsyncClient(timeout=120.0) as client:
            response = await client.post(
                LLAMA_SERVER_URL,
                json={
                    "messages": [
                        {"role": "system", "content": system_prompt},
                        {"role": "user", "content": user_prompt}
                    ],
                    "temperature": 0.2,
                    "max_tokens": 1200
                }
            )
            response.raise_for_status()
            raw_content = response.json()["choices"][0]["message"]["content"].strip()

            start_idx = raw_content.find('[')
            end_idx = raw_content.rfind(']')
            if start_idx == -1 or end_idx == -1:
                raise HTTPException(status_code=500, detail="Invalid JSON format in LLM output.")

            parsed_questions = json.loads(raw_content[start_idx : end_idx + 1])
            formatted_questions: List[TriviaQuestion] = []
            seen_questions = set()

            for item in parsed_questions:
                q_text = str(item.get("question", "")).strip()
                if not q_text or q_text.lower() in seen_questions:
                    continue

                opts = item.get("options", {})
                raw_options = [
                    ("A", str(opts.get("A", ""))),
                    ("B", str(opts.get("B", ""))),
                    ("C", str(opts.get("C", ""))),
                    ("D", str(opts.get("D", "")))
                ]

                # Ensure 4 non-empty options exist
                opt_values = [v for _, v in raw_options if v.strip()]
                if len(set(opt_values)) < 4:
                    continue

                raw_correct_key = str(item.get("correct_answer", "A")).upper()
                correct_text = next((t for k, t in raw_options if k == raw_correct_key), raw_options[0][1])

                seen_questions.add(q_text.lower())

                # Shuffle options and re-map correct answer key
                option_texts = [t for _, t in raw_options if t.strip()]
                random.shuffle(option_texts)
                new_opts = {
                    "A": option_texts[0],
                    "B": option_texts[1],
                    "C": option_texts[2],
                    "D": option_texts[3]
                }
                new_correct_key = next((k for k, v in new_opts.items() if v == correct_text), "A")

                # Dynamic Verification Data
                v_source = item.get("source_name") or ""
                v_url = item.get("source_url") or ""
                v_excerpt = item.get("source_excerpt") or str(item.get("explanation", ""))
                v_fact = item.get("verified_fact") or str(item.get("explanation", ""))

                formatted_questions.append(
                    TriviaQuestion(
                        id=len(formatted_questions) + 1,
                        question=q_text,
                        options=QuestionOption(**new_opts),
                        correct_answer=new_correct_key,
                        explanation=str(item.get("explanation", "")),
                        verification_status="pending_review",
                        verification_source=str(v_source),
                        verification_url=str(v_url),
                        verification_excerpt=str(v_excerpt),
                        verified_fact=str(v_fact),
                        as_of_date=current_today
                    )
                )

                if len(formatted_questions) >= req.num_questions:
                    break

            final_status = "success" if len(formatted_questions) >= req.num_questions else "partial_success"

            return TriviaResponse(
                status=final_status,
                batch_id=req.batch_id or "prosports_batch",
                topic=resolved_topic,
                difficulty=req.difficulty,
                total_generated=len(formatted_questions),
                questions=formatted_questions
            )

    except HTTPException:
        raise
    except Exception as e:
        err_detail = f"{type(e).__name__}: {str(e)}" if str(e) else type(e).__name__
        raise HTTPException(status_code=500, detail=f"Generation Error: {err_detail}")

@app.get("/api/v1/prosports/spec")
def get_spec():
    return FileResponse("/var/www/geoplay/prosports_spec.md", media_type="text/markdown")
