#!/usr/bin/env python3
"""
TIER-1 AGENTIC RAG - Multi-Hop Retrieval + LLM Reranking
=========================================================
What elevates RAG from "search tool" to "intelligence analyst":

1. LLM RERANKING - Cross-encode top-K results for precision
   - Initial ANN search returns 20 candidates
   - LLM scores each result against the query
   - Returns top-5 highest-confidence hits with reasoning

2. MULTI-HOP RETRIEVAL - Chain-of-thought investigation
   - "Token X has this scam pattern → same deployer → check their other tokens → any also scams?"
   - Agent plans retrieval steps, executes, synthesizes

3. REFLECTION LOOP - Self-correcting search
   - "Results look low-confidence. Reformulate query with different keywords."
   - "Found pattern A. Did I check pattern B which is often paired with A?"

4. EVIDENCE WEIGHTING - Confidence scoring per source
   - Curated patterns: 0.9 weight
   - REKT reports: 0.85 weight
   - Community reports: 0.6 weight
   - Automated scan results: 0.7 weight

5. STREAMING RESPONSE - Progressive disclosure
   - Stream findings as they're discovered
"""

import json
import logging
import os
import re
from collections.abc import AsyncGenerator
from datetime import UTC, datetime
from typing import Any

import httpx

logger = logging.getLogger(__name__)

# AI Router config
OPENROUTER_KEY = os.getenv("OPENROUTER_API_KEY", "")
# Decode base64 LLM key if present, otherwise use plain LLM_API_KEY
if os.getenv("LLM_API_KEY_B64"):
    import base64 as _b64

    os.environ["LLM_API_KEY"] = _b64.b64decode(os.getenv("LLM_API_KEY_B64")).decode()
LLM_API_KEY = os.getenv("LLM_API_KEY", OPENROUTER_KEY)
LLM_BASE_URL = os.getenv("LLM_BASE_URL", "https://api.deepseek.com/v1/chat/completions")
AI_BASE = LLM_BASE_URL if LLM_API_KEY else "https://openrouter.ai/api/v1/chat/completions"
RERANK_MODEL = os.getenv("RAG_RERANK_MODEL", os.getenv("LLM_MODEL", "deepseek-v4-flash"))
ANALYSIS_MODEL = os.getenv("RAG_ANALYSIS_MODEL", os.getenv("LLM_MODEL", "deepseek-v4-flash"))


# ══════════════════════════════════════════════════════════════════════
# LLM RERANKER
# ══════════════════════════════════════════════════════════════════════


class LLMReranker:
    """
    Cross-encode style reranking using LLM.
    Unlike pure vector similarity, the LLM:
    - Understands context and nuance
    - Can identify false positives (similar vectors, different meaning)
    - Provides reasoning for each score
    - Weighs evidence quality
    """

    RERANK_PROMPT = """You are a crypto security expert. Score how relevant each document is to the query.

Query: {query}

Documents:
{documents}

For each document, output a JSON object with:
- "id": document ID
- "score": 0.0-1.0 (how relevant to the query)
- "reasoning": one sentence explaining the relevance
- "flags": any red flags or concerns about the match quality

Return a JSON list sorted by score descending. Only return the JSON, nothing else."""

    def __init__(self, api_key: str | None = None):
        self.api_key = api_key or LLM_API_KEY

    async def rerank(
        self,
        query: str,
        documents: list[dict[str, Any]],
        top_k: int = 5,
    ) -> list[dict[str, Any]]:
        """
        Rerank search results using LLM cross-encoding.
        Takes top 20 vector results, returns top-K reranked.
        """
        if not self.api_key or not documents:
            return documents[:top_k]

        # Prepare document list for LLM
        doc_strings = []
        for i, doc in enumerate(documents[:20]):
            # Extract relevant fields
            content = doc.get("content", "")[:500]
            source = doc.get("source", doc.get("metadata", {}).get("source", ""))
            severity = doc.get("severity", doc.get("metadata", {}).get("severity", ""))
            sim = doc.get("similarity", 0)
            doc_strings.append(
                f"[{i}] ID={doc['id']} | Source={source} | Severity={severity} | "
                f"VectorSim={sim:.3f} | Content: {content}"
            )

        prompt = self.RERANK_PROMPT.format(
            query=query[:500],
            documents="\n\n".join(doc_strings),
        )

        try:
            async with httpx.AsyncClient(timeout=30) as client:
                resp = await client.post(
                    AI_BASE,
                    headers={
                        "Authorization": f"Bearer {self.api_key}",
                        "Content-Type": "application/json",
                    },
                    json={
                        "model": RERANK_MODEL,
                        "messages": [{"role": "user", "content": prompt}],
                        "temperature": 0.1,
                        "max_tokens": 1000,
                    },
                )
                resp.raise_for_status()
                data = resp.json()
                text = data["choices"][0]["message"]["content"]

                # Extract JSON from response
                json_match = re.search(r"\[.*\]", text, re.DOTALL)
                if json_match:
                    reranked = json.loads(json_match.group(0))

                    # Merge back with original documents
                    doc_map = {d["id"]: d for d in documents}
                    results = []
                    for item in reranked:
                        doc_id = item.get("id", "")
                        if doc_id in doc_map:
                            results.append(
                                {
                                    **doc_map[doc_id],
                                    "llm_score": item.get("score", 0),
                                    "llm_reasoning": item.get("reasoning", ""),
                                    "llm_flags": item.get("flags", ""),
                                    "confidence": round(
                                        0.6 * item.get("score", 0) + 0.4 * doc_map[doc_id].get("similarity", 0),
                                        4,
                                    ),
                                }
                            )

                    results.sort(key=lambda x: x.get("confidence", 0), reverse=True)
                    return results[:top_k]

        except Exception as e:
            logger.warning(f"LLM reranking failed: {e}")

        # Fallback: return top-K by vector similarity
        return sorted(documents, key=lambda x: x.get("similarity", 0), reverse=True)[:top_k]


# ══════════════════════════════════════════════════════════════════════
# MULTI-HOP RETRIEVAL AGENT
# ══════════════════════════════════════════════════════════════════════


class MultiHopRetrievalAgent:
    """
    Agentic RAG that performs multi-hop investigation.

    Pattern:
      1. User asks "Is this token a scam?"
      2. Agent plans: [check scam patterns] → [check deployer] → [check deployer's other tokens] → [synthesize]
      3. Each hop feeds into the next
      4. Final synthesis with confidence score

    Hops:
      - scam_pattern: Check against known scam DB
      - code_audit: Analyze contract code
      - deployer_check: Investigate deployer wallet
      - related_tokens: Find tokens by same deployer
      - liquidity_check: Check LP lock/burn status
      - holder_analysis: Analyze holder distribution
    """

    PLANNING_PROMPT = """You are a crypto forensics expert. Plan a multi-step investigation for this query.

Query: {query}
Context: {context}

Available investigation steps:
- scam_pattern: Semantic search against known scam database
- code_audit: Analyze contract code for vulnerabilities
- deployer_check: Investigate the deployer wallet history
- related_tokens: Find other tokens created by same deployer
- transaction_analysis: Analyze transaction patterns
- wallet_clustering: Find related wallets via graph analysis

Return a JSON plan with:
{{
  "steps": [
    {{"hop": 1, "action": "step_name", "query": "specific search query", "reasoning": "why this step"}}
  ],
  "synthesis_question": "the ultimate question to answer"
}}

Return only JSON, nothing else."""

    SYNTHESIS_PROMPT = """You are a crypto security analyst. Synthesize findings from a multi-step investigation.

Original Query: {query}

Investigation Steps and Results:
{findings}

Create a comprehensive assessment with:
1. OVERALL RISK: LOW/MEDIUM/HIGH/CRITICAL
2. CONFIDENCE: 0.0-1.0
3. EVIDENCE: Key findings supporting the assessment
4. SCAM PATTERNS FOUND: List of matched patterns
5. RELATED THREATS: Connected wallets/tokens that may also be risky
6. RECOMMENDATION: What action to take

Return as JSON. Be precise and evidence-based."""

    def __init__(self, api_key: str | None = None):
        self.api_key = api_key or LLM_API_KEY
        self.reranker = LLMReranker(api_key)

    async def investigate(
        self,
        query: str,
        context: dict[str, Any] | None = None,
        max_hops: int = 3,
    ) -> dict[str, Any]:
        """
        Multi-hop investigation pipeline.
        Returns structured findings with evidence trail.
        """
        from app.crypto_embeddings import get_embedder

        embedder = await get_embedder()
        context = context or {}

        # Step 1: Plan the investigation
        plan = await self._plan(query, context, max_hops)

        # Step 2: Execute each hop
        findings = []
        evidence_chain = []

        for step in plan.get("steps", []):
            hop_result = await self._execute_hop(step, findings, evidence_chain, embedder)
            findings.append(
                {
                    "hop": step["hop"],
                    "action": step["action"],
                    "query": step["query"],
                    "result": hop_result,
                }
            )
            evidence_chain.extend(hop_result.get("evidence", []))

        # Step 3: Rerank all collected evidence
        all_docs = []
        for f in findings:
            all_docs.extend(f.get("result", {}).get("documents", []))

        reranked = await self.reranker.rerank(query, all_docs, top_k=10)

        # Step 4: Synthesize final assessment
        synthesis = await self._synthesize(query, findings, reranked)

        return {
            "query": query,
            "investigation_plan": plan,
            "findings": findings,
            "top_evidence": reranked[:5],
            "synthesis": synthesis,
            "confidence": synthesis.get("confidence", 0),
            "risk_level": synthesis.get("overall_risk", "UNKNOWN"),
            "investigated_at": datetime.now(UTC).isoformat(),
        }

    async def _plan(self, query: str, context: dict, max_hops: int) -> dict:
        """Plan multi-hop investigation steps using LLM."""
        if not self.api_key:
            # Default plan without LLM
            return {
                "steps": [
                    {
                        "hop": 1,
                        "action": "scam_pattern",
                        "query": query,
                        "reasoning": "Check against known scam database",
                    },
                    {
                        "hop": 2,
                        "action": "code_audit",
                        "query": f"code audit {context.get('name', '')}",
                        "reasoning": "Analyze contract code patterns",
                    },
                ],
                "synthesis_question": f"Is there evidence this is a scam? {query}",
            }

        prompt = self.PLANNING_PROMPT.format(
            query=query,
            context=json.dumps(context)[:500],
        )

        try:
            async with httpx.AsyncClient(timeout=30) as client:
                resp = await client.post(
                    AI_BASE,
                    headers={
                        "Authorization": f"Bearer {self.api_key}",
                        "Content-Type": "application/json",
                    },
                    json={
                        "model": RERANK_MODEL,
                        "messages": [{"role": "user", "content": prompt}],
                        "temperature": 0.2,
                        "max_tokens": 500,
                    },
                )
                resp.raise_for_status()
                text = resp.json()["choices"][0]["message"]["content"]
                json_match = re.search(r"\{.*\}", text, re.DOTALL)
                if json_match:
                    plan = json.loads(json_match.group(0))
                    plan["steps"] = plan.get("steps", [])[:max_hops]
                    return plan
        except Exception as e:
            logger.warning(f"Planning failed: {e}")

        return {
            "steps": [
                {"hop": 1, "action": "scam_pattern", "query": query, "reasoning": "default"},
            ],
            "synthesis_question": query,
        }

    async def _execute_hop(
        self,
        step: dict,
        previous_findings: list,
        evidence_chain: list,
        embedder,
    ) -> dict:
        """Execute a single investigation hop."""
        from app.rag_service import search_multi_collection, search_similar

        action = step["action"]
        query = step["query"]

        try:
            if action == "scam_pattern":
                results = await search_multi_collection(query, collections=["known_scams", "scam_patterns"], limit=10)
            elif action == "code_audit":
                results = await search_similar(query, "contract_audits", limit=10)
            elif action == "deployer_check":
                results = await search_similar(query, "wallet_profiles", limit=10)
            elif action == "related_tokens":
                results = await search_similar(query, "token_analysis", limit=10)
            elif action == "transaction_analysis":
                results = await search_similar(query, "transaction_patterns", limit=10)
            else:
                # Generic search across all collections
                results = await search_multi_collection(query, limit=10)

            return {
                "action": action,
                "query": query,
                "documents": results,
                "count": len(results),
                "evidence": [
                    {
                        "id": r["id"],
                        "similarity": r.get("similarity", 0),
                        "content": r.get("content", "")[:300],
                    }
                    for r in results[:3]
                ],
            }
        except Exception as e:
            logger.warning(f"Hop {action} failed: {e}")
            return {"action": action, "error": str(e), "documents": [], "evidence": []}

    async def _synthesize(
        self,
        query: str,
        findings: list,
        reranked: list,
    ) -> dict:
        """Synthesize final assessment from all findings."""
        if not self.api_key:
            # Heuristic synthesis without LLM
            high_risk = sum(1 for r in reranked if r.get("severity", "").lower() in ("critical", "high"))
            total = len(reranked) or 1
            return {
                "overall_risk": "HIGH" if high_risk / total > 0.3 else "MEDIUM",
                "confidence": min(0.9, high_risk / max(1, total)),
                "evidence_count": len(reranked),
                "scam_patterns_found": [r.get("metadata", {}).get("name", "") for r in reranked[:5]],
                "recommendation": "Avoid - high risk indicators detected" if high_risk > 0 else "Proceed with caution",
            }

        # Build findings summary
        finding_text = ""
        for f in findings:
            finding_text += f"\nHop {f['hop']}: {f['action']} - {f['result'].get('count', 0)} results\n"
            for doc in f.get("result", {}).get("documents", [])[:3]:
                finding_text += f"  - {doc.get('content', '')[:200]}\n"

        prompt = self.SYNTHESIS_PROMPT.format(
            query=query,
            findings=finding_text[:3000],
        )

        try:
            async with httpx.AsyncClient(timeout=30) as client:
                resp = await client.post(
                    AI_BASE,
                    headers={
                        "Authorization": f"Bearer {self.api_key}",
                        "Content-Type": "application/json",
                    },
                    json={
                        "model": ANALYSIS_MODEL,
                        "messages": [{"role": "user", "content": prompt}],
                        "temperature": 0.2,
                        "max_tokens": 800,
                        "response_format": {"type": "json_object"},
                    },
                )
                resp.raise_for_status()
                text = resp.json()["choices"][0]["message"]["content"]
                json_match = re.search(r"\{.*\}", text, re.DOTALL)
                if json_match:
                    return json.loads(json_match.group(0))
        except Exception as e:
            logger.warning(f"Synthesis LLM failed: {e}")

        return {
            "overall_risk": "UNKNOWN",
            "confidence": 0.3,
            "error": "Synthesis failed",
        }


# ══════════════════════════════════════════════════════════════════════
# STREAMING SEARCH (for real-time UX)
# ══════════════════════════════════════════════════════════════════════


async def stream_rag_search(
    query: str,
    collection: str = "all",
    limit: int = 5,
) -> AsyncGenerator[str, None]:
    """
    Streaming RAG search - yields results as they're discovered.
    Pattern: "thinking..." → "found N matches..." → "reranking..." → results
    """
    from app.rag_service import search_multi_collection, search_similar

    # Phase 1: Quick keyword pre-filter (instant)
    yield json.dumps({"phase": "quick_scan", "status": "searching"}) + "\n"

    # Phase 2: Vector search
    yield json.dumps({"phase": "vector_search", "status": "embedding_query"}) + "\n"

    if collection == "all":
        results = await search_multi_collection(query, limit=limit * 4)
    else:
        results = await search_similar(query, collection, limit=limit * 4)

    yield (
        json.dumps(
            {
                "phase": "vector_search",
                "status": "complete",
                "candidates": len(results),
            }
        )
        + "\n"
    )

    # Phase 3: Reranking
    if results:
        yield json.dumps({"phase": "reranking", "status": "cross_encoding"}) + "\n"

        reranker = LLMReranker()
        reranked = await reranker.rerank(query, results, top_k=limit)

        yield (
            json.dumps(
                {
                    "phase": "complete",
                    "results": reranked[:limit],
                    "total_candidates": len(results),
                    "top_result": reranked[0] if reranked else None,
                }
            )
            + "\n"
        )
    else:
        yield (
            json.dumps(
                {
                    "phase": "complete",
                    "results": [],
                    "total_candidates": 0,
                }
            )
            + "\n"
        )


# ══════════════════════════════════════════════════════════════════════
# REAL-TIME TOKEN MONITOR
# ══════════════════════════════════════════════════════════════════════


class RealTimeTokenMonitor:
    """
    Stream new token deployments through the scam detection pipeline.
    When a new token is found:
      1. Extract features (name, symbol, deployer, initial txns)
      2. Quick keyword scan (instant)
      3. Embed and compare against scam DB
      4. Multi-hop if high-risk
      5. Alert if scam detected
    """

    def __init__(self):
        self.processed = set()
        self._processed_order = []  # FIFO order for eviction
        self._max_processed = 10000
        self.alerts = []

    async def scan_new_token(self, token_data: dict) -> dict[str, Any]:
        """Quick-look scan of a newly deployed token."""
        from app.rag_service import detect_scam_patterns

        address = token_data.get("address", "")
        if address in self.processed:
            return {"status": "already_processed", "address": address}
        self.processed.add(address)
        self._processed_order.append(address)

        # Evict oldest half when over limit (FIFO)
        if len(self.processed) > self._max_processed:
            evict_count = len(self._processed_order) // 2
            for old_addr in self._processed_order[:evict_count]:
                self.processed.discard(old_addr)
            self._processed_order = self._processed_order[evict_count:]

        # Phase 1: Quick keyword scan (sub-second)
        quick = await detect_scam_patterns(token_data)
        quick_matches = quick.get("quick_matches", [])

        # Phase 2: If quick matches found → deep semantic scan
        deep_matches = quick.get("deep_matches", [])

        # Phase 3: Risk assessment
        critical_matches = [m for m in quick_matches if m.get("severity") == "critical"]
        high_matches = [m for m in quick_matches if m.get("severity") == "high"]

        risk = "low"
        if critical_matches:
            risk = "critical"
        elif high_matches or deep_matches:
            risk = "high"
        elif quick_matches:
            risk = "medium"

        result = {
            "address": address,
            "name": token_data.get("name", ""),
            "symbol": token_data.get("symbol", ""),
            "chain": token_data.get("chain", "solana"),
            "risk": risk,
            "quick_matches": quick_matches,
            "deep_matches": deep_matches,
            "highest_threat": quick.get("highest_threat", "none"),
            "scanned_at": datetime.now(UTC).isoformat(),
        }

        # Phase 4: Alert if high risk
        if risk in ("critical", "high"):
            self.alerts.append(result)
            # Keep only last 100 alerts
            if len(self.alerts) > 100:
                self.alerts = self.alerts[-100:]

        return result

    def get_recent_alerts(self, limit: int = 20) -> list[dict]:
        return self.alerts[-limit:]


# ══════════════════════════════════════════════════════════════════════
# SINGLETONS
# ══════════════════════════════════════════════════════════════════════

_reranker: LLMReranker | None = None
_agent: MultiHopRetrievalAgent | None = None
_monitor: RealTimeTokenMonitor | None = None


def get_reranker() -> LLMReranker:
    global _reranker
    if _reranker is None:
        _reranker = LLMReranker()
    return _reranker


def get_agent() -> MultiHopRetrievalAgent:
    global _agent
    if _agent is None:
        _agent = MultiHopRetrievalAgent()
    return _agent


def get_monitor() -> RealTimeTokenMonitor:
    global _monitor
    if _monitor is None:
        _monitor = RealTimeTokenMonitor()
    return _monitor
