"""
Transaction Analyzer — the core diagnostic engine for CrashSense.

Takes raw blockchain transaction context, enriches it with pattern matching
and RAG docs, then feeds it to the LLM for human-readable diagnosis.
"""

from __future__ import annotations

import re
from pathlib import Path
from typing import Any, Dict, List, Optional

from .chain_fetcher import ChainFetcher
from .llm_adapter import LLMAdapter
from ..config import load_config


class TxAnalyzer:
    """Analyze a blockchain transaction and produce a diagnosis."""

    def __init__(
        self,
        provider: str = "gemini",
        model: Optional[str] = None,
        cfg: Optional[Dict[str, Any]] = None,
    ) -> None:
        self.cfg = cfg or load_config()
        self.llm = LLMAdapter(provider=provider, model=model, cfg=self.cfg)
        self.fetcher = ChainFetcher(cfg=self.cfg)

    # ── Public API ────────────────────────────────────────────────────────

    def diagnose(self, tx_hash: str) -> Dict[str, Any]:
        """
        Full pipeline: fetch → pattern-match → LLM analysis → structured result.
        """
        ctx = self.fetcher.fetch_full_context(tx_hash)
        if "error" in ctx:
            return {"tx_context": ctx, "analysis": {"explanation": ctx["error"], "root_cause": "fetch_error", "suggested_fix": ""}}

        # Pre-analysis pattern matching
        patterns = self._match_patterns(ctx)

        # Optional RAG context
        rag_ctx = self._load_rag_context(ctx)

        # Build the LLM prompt
        prompt = self._build_prompt(ctx, patterns, rag_ctx)

        # Call LLM
        analysis = self.llm.analyze(prompt)

        return {
            "tx_context": ctx,
            "patterns_matched": patterns,
            "analysis": analysis,
        }

    def diagnose_raw(self, raw_text: str) -> Dict[str, Any]:
        """
        Analyze raw error text / log (no on-chain fetch).
        Useful for pasting error messages directly.
        """
        patterns = self._match_raw_patterns(raw_text)
        rag_ctx = ""
        if self.cfg.get("rag", {}).get("enabled"):
            docs = self._load_rag_docs()
            if docs:
                top = self.llm.retrieve(raw_text[:2000], docs, top_k=3)
                if top:
                    parts = [f"[ctx#{i} score={s:.1f}]\n{chunk}" for i, (chunk, s) in enumerate(top, 1)]
                    rag_ctx = "\n\n".join(parts)

        prompt = (
            "A DeFi user encountered the following error or transaction data:\n\n"
            f"```\n{raw_text[:4000]}\n```\n\n"
        )
        if patterns:
            prompt += f"Known patterns detected: {', '.join(patterns)}\n\n"
        if rag_ctx:
            prompt += f"Reference context:\n{rag_ctx}\n\n"
        prompt += (
            "Explain what happened in plain language that a non-technical DeFi user can understand. "
            "Identify the root cause and suggest a concrete fix.\n\n"
            "Format your response with these sections:\n"
            "## Root Cause\n## Explanation\n## Suggested Fix"
        )

        analysis = self.llm.analyze(prompt)
        return {"raw_input": raw_text[:500], "patterns_matched": patterns, "analysis": analysis}

    # ── Pattern matching ──────────────────────────────────────────────────

    @staticmethod
    def _match_patterns(ctx: Dict[str, Any]) -> List[str]:
        """Match well-known DeFi failure patterns from tx context."""
        patterns: List[str] = []
        status = ctx.get("status", "")
        revert = (ctx.get("revert_reason") or "").lower()
        func = (ctx.get("function_called") or "").lower()
        gas_pct = ctx.get("gas_utilisation_pct", 0)

        if status == "failed":
            patterns.append("TRANSACTION_FAILED")

        # Gas issues
        if gas_pct >= 99.0:
            patterns.append("OUT_OF_GAS")
        elif gas_pct >= 95.0:
            patterns.append("NEAR_GAS_LIMIT")

        # Revert reason patterns
        revert_map = {
            "insufficient balance": "INSUFFICIENT_BALANCE",
            "insufficient allowance": "INSUFFICIENT_ALLOWANCE",
            "erc20: transfer amount exceeds balance": "INSUFFICIENT_BALANCE",
            "erc20: transfer amount exceeds allowance": "INSUFFICIENT_ALLOWANCE",
            "ds-math-sub-underflow": "ARITHMETIC_UNDERFLOW",
            "safeerc20: low-level call failed": "SAFE_TRANSFER_FAILED",
            "uniswapv2: insufficient_output_amount": "SLIPPAGE_EXCEEDED",
            "uniswapv2: insufficient_liquidity": "INSUFFICIENT_LIQUIDITY",
            "uniswapv2: expired": "DEADLINE_EXPIRED",
            "too little received": "SLIPPAGE_EXCEEDED",
            "too much requested": "SLIPPAGE_EXCEEDED",
            "stf": "SAFE_TRANSFER_FAILED",
            "overflow": "ARITHMETIC_OVERFLOW",
            "underflow": "ARITHMETIC_UNDERFLOW",
            "panic": "PANIC_ERROR",
            "ownable: caller is not the owner": "ACCESS_DENIED",
            "not owner": "ACCESS_DENIED",
            "paused": "CONTRACT_PAUSED",
            "reentrancy": "REENTRANCY_GUARD",
            "already initialized": "ALREADY_INITIALIZED",
            "transfer failed": "TRANSFER_FAILED",
            "expired": "DEADLINE_EXPIRED",
            "blacklisted": "BLACKLISTED_ADDRESS",
        }
        for pattern_text, label in revert_map.items():
            if pattern_text in revert:
                patterns.append(label)

        # Function-based hints
        specific = [p for p in patterns if p != "TRANSACTION_FAILED"]
        if "swap" in func and not specific:
            patterns.append("SWAP_FAILURE")
        if "approve" in func:
            patterns.append("APPROVAL_TX")
        if "transfer" in func and "INSUFFICIENT" not in " ".join(patterns):
            patterns.append("TOKEN_TRANSFER")

        # Value-based
        if ctx.get("value_wei", 0) > 0 and not ctx.get("to"):
            patterns.append("CONTRACT_CREATION")

        return list(dict.fromkeys(patterns))  # deduplicate, preserve order

    @staticmethod
    def _match_raw_patterns(text: str) -> List[str]:
        """Match patterns from raw error text."""
        lower = text.lower()
        patterns: List[str] = []
        checks = {
            "execution reverted": "EXECUTION_REVERTED",
            "out of gas": "OUT_OF_GAS",
            "insufficient funds": "INSUFFICIENT_FUNDS",
            "insufficient balance": "INSUFFICIENT_BALANCE",
            "insufficient allowance": "INSUFFICIENT_ALLOWANCE",
            "nonce too low": "NONCE_TOO_LOW",
            "nonce too high": "NONCE_TOO_HIGH",
            "replacement transaction underpriced": "UNDERPRICED",
            "already known": "TX_ALREADY_KNOWN",
            "gas too low": "GAS_TOO_LOW",
            "max fee per gas less than block base fee": "GAS_PRICE_TOO_LOW",
            "slippage": "SLIPPAGE_EXCEEDED",
            "deadline": "DEADLINE_EXPIRED",
        }
        for needle, label in checks.items():
            if needle in lower:
                patterns.append(label)
        return patterns

    # ── Prompt building ───────────────────────────────────────────────────

    def _build_prompt(self, ctx: Dict[str, Any], patterns: List[str], rag_ctx: str) -> str:
        """Build a rich, structured prompt for the LLM."""
        currency = self.fetcher.chain_info.get("currency", "ETH")

        parts = [
            "Analyze this failed/reverted blockchain transaction and explain what went wrong.\n",
            f"**Chain:** {ctx['chain']}",
            f"**Tx Hash:** {ctx['tx_hash']}",
            f"**From:** {ctx['from']}",
            f"**To:** {ctx['to']}",
            f"**Value:** {ctx['value_eth']} {currency}",
            f"**Function Called:** {ctx['function_called']}",
            f"**Status:** {ctx['status']}",
            f"**Gas Limit:** {ctx['gas_limit']:,}",
            f"**Gas Used:** {ctx['gas_used']:,} ({ctx['gas_utilisation_pct']}%)",
            f"**Gas Price:** {ctx['gas_price_gwei']} Gwei",
            f"**Tx Fee:** {ctx['tx_fee_eth']} {currency}",
            f"**Block:** {ctx['block_number']:,}",
            f"**Nonce:** {ctx['nonce']}",
        ]

        if ctx.get("revert_reason"):
            parts.append(f"\n**Revert Reason:** {ctx['revert_reason']}")

        if ctx.get("log_summaries"):
            events = ", ".join(f"{l['event']}@{l['contract'][:10]}…" for l in ctx["log_summaries"][:8])
            parts.append(f"**Events emitted:** {events}")

        if patterns:
            parts.append(f"\n**Detected patterns:** {', '.join(patterns)}")

        if ctx.get("input_data") and ctx["input_data"] != "0x":
            parts.append(f"\n**Input data (truncated):** {ctx['input_data']}")

        if rag_ctx:
            parts.append(f"\n**Reference Documentation:**\n{rag_ctx}")

        parts.append(
            "\n\nProvide your answer with these exact section headers:\n"
            "## Root Cause\n"
            "(one-sentence technical root cause)\n\n"
            "## Explanation\n"
            "(clear, human-readable explanation that a non-technical DeFi user can understand)\n\n"
            "## Suggested Fix\n"
            "(step-by-step actionable fix the user can follow)"
        )

        return "\n".join(parts)

    # ── RAG helpers ───────────────────────────────────────────────────────

    def _load_rag_context(self, ctx: Dict[str, Any]) -> str:
        """Load and retrieve relevant RAG chunks for this tx."""
        if not self.cfg.get("rag", {}).get("enabled"):
            return ""
        docs = self._load_rag_docs()
        if not docs:
            return ""

        query = f"{ctx.get('revert_reason', '')} {ctx.get('function_called', '')} {' '.join(ctx.get('patterns_matched', []) if isinstance(ctx.get('patterns_matched'), list) else [])}"
        top_k = int(self.cfg.get("rag", {}).get("top_k", 3))
        retrieved = self.llm.retrieve(query.strip(), docs, top_k=top_k)
        if not retrieved:
            return ""

        parts = [f"[ctx#{i} score={s:.1f}]\n{chunk}" for i, (chunk, s) in enumerate(retrieved, 1)]
        return "\n\n".join(parts)

    def _load_rag_docs(self) -> List[str]:
        """Load and chunk RAG documents."""
        rag_cfg = self.cfg.get("rag", {})
        paths = rag_cfg.get("docs") or []
        size = int(rag_cfg.get("chunk_chars", 800))
        overlap = int(rag_cfg.get("chunk_overlap", 120))
        exts = {".md", ".txt", ".json", ".yaml", ".yml", ".toml", ".py"}

        chunks: List[str] = []
        for p in paths:
            try:
                path = Path(p)
                if path.is_file():
                    txt = path.read_text(encoding="utf-8", errors="ignore")
                    chunks.extend(_chunk_text(txt, size, overlap))
                elif path.is_dir():
                    for child in path.rglob("*"):
                        if child.is_file() and child.suffix.lower() in exts:
                            try:
                                txt = child.read_text(encoding="utf-8", errors="ignore")
                                chunks.extend(_chunk_text(txt, size, overlap))
                                if len(chunks) >= 200:
                                    break
                            except Exception:
                                continue
            except Exception:
                continue
        return chunks[:200]


# ── Module helpers ────────────────────────────────────────────────────────

def _chunk_text(text: str, size: int, overlap: int) -> List[str]:
    if size <= 0:
        return [text]
    chunks: List[str] = []
    start = 0
    n = len(text)
    while start < n:
        end = min(n, start + size)
        chunks.append(text[start:end])
        if end == n:
            break
        start = max(end - overlap, start + 1)
    return chunks
