"""
SENTINEL AI - Self-Training Scam Classifier
============================================

Turns 5,000+ historical token scans into a self-improving ML model.
Every new scan feeds back into training. The model gets smarter over time.

Architecture:
  Historical scans → Feature extraction → XGBoost training → Model on disk
  New scan → Feature extraction → Model prediction → AI risk score (0-100)
  Confirmed scam → Weight boost → Retrain trigger

Features extracted from all 45 enrichments + market data + SENTINEL modules.
The model learns which COMBINATIONS of signals predict scams, catching
patterns that static rules miss entirely.

Premium feature: "AI-Powered Risk Score" - ML confidence alongside rules-based score.
"""

import json
import logging
import os
import pickle
from datetime import UTC, datetime
from typing import Any

import numpy as np

logger = logging.getLogger("sentinel.ai")

MODEL_DIR = os.path.join(os.path.dirname(__file__), "..", "data", "models")
MODEL_PATH = os.path.join(MODEL_DIR, "scam_classifier_xgb.pkl")
FEATURE_NAMES_PATH = os.path.join(MODEL_DIR, "scam_classifier_features.json")

# ──────────────────────────────────────────────────────────────
# Feature Extraction - 80+ features from enrichment data
# ──────────────────────────────────────────────────────────────


def extract_features(scan_result: dict[str, Any]) -> dict[str, float]:
    """Extract ML features from a scan result. Returns dict of feature_name → value."""
    f = {}
    free = scan_result.get("free", scan_result) if isinstance(scan_result, dict) else {}

    # ── Market data features (16 features) ──
    f["price_usd"] = float(free.get("price_usd", 0) or 0)
    f["volume_24h"] = float(free.get("volume_24h", 0) or 0)
    f["liquidity_usd"] = float(free.get("liquidity_usd", 0) or 0)
    f["market_cap"] = float(free.get("market_cap", 0) or 0)
    f["age_hours"] = float(free.get("age_hours", 0) or 0)
    f["price_change_5m"] = float(free.get("price_change_5m", 0) or 0)
    f["price_change_1h"] = float(free.get("price_change_1h", 0) or 0)
    f["price_change_24h"] = float(free.get("price_change_24h", 0) or 0)
    f["tx_count"] = float(free.get("tx_count", 0) or 0)
    f["volume_to_liquidity_ratio"] = f["volume_24h"] / max(f["liquidity_usd"], 1)
    f["market_cap_to_liquidity_ratio"] = f["market_cap"] / max(f["liquidity_usd"], 1)
    f["has_price_data"] = 1.0 if f["price_usd"] > 0 else 0.0
    f["has_liquidity"] = 1.0 if f["liquidity_usd"] > 0 else 0.0
    f["is_very_new"] = 1.0 if f["age_hours"] < 1 else 0.0
    f["is_new"] = 1.0 if f["age_hours"] < 24 else 0.0
    f["price_volatility"] = abs(f["price_change_1h"]) + abs(f["price_change_24h"])

    # ── Holder features (8 features) ──
    holders = free.get("holders", {}) or {}
    if isinstance(holders, dict):
        f["holder_count"] = float(holders.get("total", holders.get("count", 0)) or 0)
        f["top10_pct"] = float(holders.get("top_10_percentage", holders.get("top10_pct", 0)) or 0)
        f["top1_pct"] = float(holders.get("top_1_percentage", holders.get("top1_pct", 0)) or 0)
        f["holder_concentration_high"] = 1.0 if f["top10_pct"] > 80 else 0.0
        f["holder_concentration_critical"] = 1.0 if f["top10_pct"] > 95 else 0.0
        f["has_holder_data"] = 1.0 if f["holder_count"] > 0 else 0.0
        f["few_holders"] = 1.0 if 0 < f["holder_count"] < 20 else 0.0
        f["many_holders"] = 1.0 if f["holder_count"] > 500 else 0.0

    # ── Honeypot / trade simulation (8 features) ──
    sim = free.get("simulation", {}) or {}
    if isinstance(sim, dict):
        f["honeypot_detected"] = 1.0 if sim.get("is_honeypot") or sim.get("honeypot") else 0.0
        f["buy_success"] = 1.0 if sim.get("buy_success", True) else 0.0
        f["sell_success"] = 1.0 if sim.get("sell_success", True) else 0.0
        f["buy_tax_pct"] = float(sim.get("buy_tax_pct", 0) or 0)
        f["sell_tax_pct"] = float(sim.get("sell_tax_pct", 0) or 0)
        f["tax_high"] = 1.0 if f["sell_tax_pct"] > 10 else 0.0
        f["tax_extreme"] = 1.0 if f["sell_tax_pct"] > 50 else 0.0
        f["has_simulation"] = 1.0 if sim else 0.0

    # ── Contract / authority features (6 features) ──
    contract = free.get("contract_verification", {}) or {}
    if isinstance(contract, dict):
        f["contract_verified"] = 1.0 if contract.get("verified") else 0.0
        f["contract_unverified"] = 1.0 if not contract.get("verified") else 0.0
        f["is_proxy"] = 1.0 if contract.get("is_proxy") or contract.get("proxy") else 0.0
        f["mint_authority_renounced"] = 1.0 if free.get("mint_authority") == "renounced" else 0.0
        f["freeze_authority_exists"] = 1.0 if free.get("freeze_authority") else 0.0
        f["has_contract_data"] = 1.0 if contract else 0.0

    # ── Deployer features (8 features) ──
    deployer = free.get("deployer", {}) or {}
    deep = free.get("deep_deployer", {}) or {}
    if isinstance(deployer, dict) or isinstance(deep, dict):
        d = {**deployer, **deep} if isinstance(deep, dict) else deployer
        f["deployer_known"] = 1.0 if d.get("address") or deployer.get("address") else 0.0
        f["deployer_risk_score"] = float(d.get("risk_score", 0) or 0)
        f["deployer_scam_count"] = float(d.get("scam_count", 0) or 0)
        f["deployer_high_risk"] = 1.0 if f["deployer_risk_score"] > 70 else 0.0
        f["deployer_critical"] = 1.0 if f["deployer_risk_score"] > 90 else 0.0
        f["multi_chain_deployer"] = 1.0 if free.get("cross_chain", {}).get("chain_count", 0) > 1 else 0.0
        f["deployer_chain_count"] = float(
            free.get("cross_chain", {}).get("chain_count", 1) if isinstance(free.get("cross_chain"), dict) else 1
        )
        f["has_deployer_data"] = 1.0 if f["deployer_known"] else 0.0

    # ── LP / liquidity lock (5 features) ──
    lp = free.get("lp_lock_multi", {}) or {}
    if isinstance(lp, dict):
        f["lp_locked"] = 1.0 if lp.get("locked") or lp.get("is_locked") else 0.0
        f["lp_lock_unconfirmed"] = 1.0 if not f["lp_locked"] and f["liquidity_usd"] > 0 else 0.0
        f["lp_lock_pct"] = float(lp.get("lock_percentage", lp.get("locked_pct", 0)) or 0)
        f["lp_has_data"] = 1.0 if lp else 0.0
        f["no_lp"] = 1.0 if f["liquidity_usd"] == 0 else 0.0

    # ── External API signals (12 features) ──
    # Birdeye
    birdeye = free.get("birdeye", {}) or {}
    f["birdeye_honeypot"] = 1.0 if isinstance(birdeye, dict) and birdeye.get("honeypot") else 0.0
    f["birdeye_rugpull"] = 1.0 if isinstance(birdeye, dict) and birdeye.get("rugpull") else 0.0

    # Honeypot.is
    hp = free.get("honeypot_is", {}) or {}
    f["honeypot_is_detected"] = 1.0 if isinstance(hp, dict) and hp.get("is_honeypot") else 0.0

    # Token Sniffer
    ts = free.get("token_sniffer", {}) or {}
    f["tokensniffer_scam"] = 1.0 if isinstance(ts, dict) and ts.get("is_scam") else 0.0
    f["tokensniffer_score"] = float(ts.get("score", 0) or 0) if isinstance(ts, dict) else 0.0

    # ChainAware AI
    ca = free.get("chainaware_ai", {}) or {}
    f["chainaware_rug_risk"] = float(ca.get("rug_probability", 0) or 0) if isinstance(ca, dict) else 0.0

    # GoPlus
    gp = free.get("goplus", {}) or {}
    f["goplus_honeypot"] = 1.0 if isinstance(gp, dict) and gp.get("is_honeypot") else 0.0

    # De.Fi
    df = free.get("defi_scanner", {}) or {}
    f["defi_honeypot"] = 1.0 if isinstance(df, dict) and df.get("honeypot") else 0.0
    f["defi_ai_score"] = float(df.get("aiScore", 50) or 50) if isinstance(df, dict) else 50.0

    # Copycat
    cc = free.get("copycat_check", {}) or {}
    f["is_copycat"] = 1.0 if isinstance(cc, dict) and cc.get("is_copycat") else 0.0

    # Volume anomaly
    va = free.get("volume_anomaly", {}) or {}
    f["volume_manipulation"] = 1.0 if isinstance(va, dict) and va.get("manipulation_detected") else 0.0

    # ── RAG / scam database features (6 features) ──
    rag = free.get("rag_scam_check", {}) or {}
    if isinstance(rag, dict):
        f["rag_scam_match"] = 1.0 if rag.get("match_found") or rag.get("is_scam") else 0.0
        f["rag_similarity"] = float(rag.get("similarity", 0) or 0)
        f["rag_similarity_high"] = 1.0 if f["rag_similarity"] > 0.8 else 0.0
        f["rag_matches_count"] = float(rag.get("match_count", 0) or 0)
        f["known_scam_address"] = 1.0 if rag.get("is_known_scam") else 0.0
        f["has_rag_data"] = 1.0 if rag else 0.0

    # ── Social / sentiment (5 features) ──
    sentiment = free.get("sentiment", {}) or {}
    f["social_volume"] = float(sentiment.get("mention_count", 0) or 0) if isinstance(sentiment, dict) else 0.0
    f["sentiment_score"] = float(sentiment.get("score", 0) or 0) if isinstance(sentiment, dict) else 0.0
    santiment = free.get("santiment", {}) or {}
    f["santiment_hype"] = 1.0 if isinstance(santiment, dict) and santiment.get("hype_detected") else 0.0
    f["santiment_viral_low_vol"] = 1.0 if isinstance(santiment, dict) and santiment.get("viral_low_volume") else 0.0
    f["has_social_data"] = 1.0 if sentiment or santiment else 0.0

    # ── Chain/network features (4 features) ──
    chain = scan_result.get("chain", "").lower()
    f["chain_solana"] = 1.0 if chain == "solana" else 0.0
    f["chain_ethereum"] = 1.0 if chain == "ethereum" else 0.0
    f["chain_bsc"] = 1.0 if chain == "bsc" else 0.0
    f["chain_base"] = 1.0 if chain == "base" else 0.0

    # ── Nansen / Arkham (4 features) ──
    nansen = free.get("nansen", {}) or {}
    f["nansen_low_holders"] = 1.0 if isinstance(nansen, dict) and nansen.get("low_holders") else 0.0
    f["nansen_net_outflow"] = 1.0 if isinstance(nansen, dict) and nansen.get("net_outflow") else 0.0
    arkham = free.get("arkham", {}) or {}
    f["arkham_scam_entity"] = 1.0 if isinstance(arkham, dict) and arkham.get("scam_entity") else 0.0
    f["has_premium_data"] = 1.0 if nansen or arkham else 0.0

    return f


# ──────────────────────────────────────────────────────────────
# Model Training
# ──────────────────────────────────────────────────────────────


class ScamClassifier:
    """XGBoost classifier trained on historical scan data."""

    def __init__(self):
        self.model = None
        self.feature_names: list[str] = []
        self.training_samples: int = 0
        self.accuracy: float = 0.0
        self.last_trained: str | None = None
        self._load()

    def _load(self):
        """Load model from disk if available."""
        if os.path.exists(MODEL_PATH):
            try:
                with open(MODEL_PATH, "rb") as f:
                    self.model = pickle.load(f)
                if os.path.exists(FEATURE_NAMES_PATH):
                    with open(FEATURE_NAMES_PATH) as f:
                        meta = json.load(f)
                        self.feature_names = meta.get("features", [])
                        self.training_samples = meta.get("samples", 0)
                        self.accuracy = meta.get("accuracy", 0.0)
                        self.last_trained = meta.get("trained_at")
                logger.info(
                    f"Loaded scam classifier: {self.training_samples} samples, "
                    f"{len(self.feature_names)} features, {self.accuracy:.1%} accuracy"
                )
            except Exception as e:
                logger.warning(f"Failed to load classifier: {e}")
                self.model = None

    def _save(self):
        """Persist model and metadata to disk."""
        os.makedirs(MODEL_DIR, exist_ok=True)
        with open(MODEL_PATH, "wb") as f:
            pickle.dump(self.model, f)
        with open(FEATURE_NAMES_PATH, "w") as f:
            json.dump(
                {
                    "features": self.feature_names,
                    "samples": self.training_samples,
                    "accuracy": self.accuracy,
                    "trained_at": datetime.now(UTC).isoformat(),
                },
                f,
            )
        logger.info(f"Saved classifier: {self.training_samples} samples, {self.accuracy:.1%} accuracy")

    def train(self, scans: list[dict[str, Any]]) -> dict[str, Any]:
        """Train on historical scan data. Each scan must have 'is_scam' label."""
        try:
            from xgboost import XGBClassifier
        except ImportError:
            logger.warning("XGBoost not installed. Install with: pip install xgboost")
            return {"status": "error", "error": "xgboost not installed"}

        # Extract features and labels
        X_list = []
        y_list = []

        for scan in scans:
            features = extract_features(scan)
            is_scam = scan.get("is_scam", False) or scan.get("verdict") == "scam"

            if not X_list:  # First sample - record feature names
                self.feature_names = sorted(features.keys())

            # Build feature vector in consistent order
            row = [features.get(name, 0.0) for name in self.feature_names]
            X_list.append(row)
            y_list.append(1 if is_scam else 0)

        if len(X_list) < 10:
            return {"status": "error", "error": f"Need at least 10 samples, got {len(X_list)}"}

        if sum(y_list) < 2:
            return {"status": "error", "error": "Need at least 2 scam samples for training"}

        X = np.array(X_list, dtype=np.float32)
        y = np.array(y_list, dtype=np.int32)

        # Handle NaN/Inf
        X = np.nan_to_num(X, nan=0.0, posinf=0.0, neginf=0.0)

        # Train
        self.model = XGBClassifier(
            n_estimators=100,
            max_depth=4,
            learning_rate=0.1,
            subsample=0.8,
            colsample_bytree=0.8,
            scale_pos_weight=(len(y) - sum(y)) / max(sum(y), 1),  # Handle class imbalance
            random_state=42,
            use_label_encoder=False,
            eval_metric="logloss",
        )
        self.model.fit(X, y)

        # Evaluate
        train_pred = self.model.predict(X)
        self.accuracy = float((train_pred == y).mean())
        self.training_samples = len(X_list)
        self.last_trained = datetime.now(UTC).isoformat()

        # Feature importance
        importance = {}
        if hasattr(self.model, "feature_importances_"):
            for name, imp in zip(self.feature_names, self.model.feature_importances_, strict=False):
                importance[name] = round(float(imp), 4)

        self._save()

        return {
            "status": "trained",
            "samples": self.training_samples,
            "scam_samples": int(sum(y)),
            "features": len(self.feature_names),
            "accuracy": round(self.accuracy, 3),
            "feature_importance": dict(sorted(importance.items(), key=lambda x: -x[1])[:15]),
        }

    def predict(self, scan_result: dict[str, Any]) -> dict[str, Any]:
        """Predict scam probability for a new scan."""
        if self.model is None:
            return {"status": "no_model", "probability": None, "confidence": 0}

        features = extract_features(scan_result)
        row = np.array([[features.get(name, 0.0) for name in self.feature_names]], dtype=np.float32)
        row = np.nan_to_num(row, nan=0.0, posinf=0.0, neginf=0.0)

        try:
            proba = self.model.predict_proba(row)[0]
            scam_prob = float(proba[1]) if len(proba) > 1 else float(proba[0])

            # Confidence based on training data quality
            confidence = min(self.accuracy * 100, 95) if self.accuracy > 0 else 50

            # Get top contributing features
            contributions = {}
            if hasattr(self.model, "feature_importances_"):
                for name, imp in zip(self.feature_names, self.model.feature_importances_, strict=False):
                    if features.get(name, 0) != 0 and imp > 0.01:
                        contributions[name] = {
                            "value": features.get(name, 0),
                            "importance": round(float(imp), 3),
                        }

            # Risk level
            if scam_prob > 0.8:
                risk_level = "critical"
            elif scam_prob > 0.6:
                risk_level = "high"
            elif scam_prob > 0.4:
                risk_level = "medium"
            elif scam_prob > 0.2:
                risk_level = "low"
            else:
                risk_level = "safe"

            return {
                "status": "ok",
                "probability": round(scam_prob, 4),
                "risk_level": risk_level,
                "ai_risk_score": round(scam_prob * 100),
                "confidence": round(confidence, 1),
                "model_samples": self.training_samples,
                "model_accuracy": round(self.accuracy, 3),
                "top_signals": dict(sorted(contributions.items(), key=lambda x: -x[1]["importance"])[:8]),
            }
        except Exception as e:
            logger.warning(f"Prediction failed: {e}")
            return {"status": "error", "probability": None, "error": str(e)}


# ──────────────────────────────────────────────────────────────
# Singleton
# ──────────────────────────────────────────────────────────────

_classifier: ScamClassifier | None = None


def get_classifier() -> ScamClassifier:
    global _classifier
    if _classifier is None:
        _classifier = ScamClassifier()
    return _classifier
