"""
Wallet Memory Engine - Orchestrator for the Wallet Memory Bank.
================================================================
Single entry point for all wallet intelligence queries. Combines:
  - Storage (ClickHouse + Redis)
  - Clustering (6 heuristics, Union-Find)
  - Labels (6 live sources + static databases)
  - Risk scoring (composite: label + cluster + deployer history)
  - Entity resolution (cross-chain linking)

Primary API:
  get_deployer_intelligence(address, chain) - what SENTINEL calls
  get_wallet_profile(address, chain) - what WalletSafe frontend calls
  flag_deployer(address, chain, reason) - the data flywheel from scans
"""

import logging
from datetime import UTC, datetime
from typing import Any

from .clustering import (
    WalletClusteringEngine,
)
from .entity_resolver import EntityResolver
from .ingestion import get_ingestion_pipeline
from .labels import get_label_service
from .risk_scoring import RiskScorer
from .storage import WalletStorage, get_storage

logger = logging.getLogger("wallet_memory.engine")


class WalletMemoryEngine:
    """
    The brain of the Wallet Memory Bank.
    Coordinates storage, clustering, labels, risk scoring, and entity resolution.
    """

    def __init__(self, storage: WalletStorage | None = None):
        self.storage = storage or get_storage()
        self.clustering = WalletClusteringEngine()
        self.labels = get_label_service()
        self.risk_scorer = RiskScorer(self.storage, self.labels, self.clustering)
        self.entity_resolver = EntityResolver(self.storage)
        self.ingestion = get_ingestion_pipeline(engine=self, storage=self.storage)

    # ── Primary API: Deployer Intelligence (SENTINEL calls this) ───

    async def get_deployer_intelligence(self, address: str, chain: str) -> dict[str, Any]:
        """
        One-shot call returning everything the token scanner needs about a deployer.

        Returns:
            {
                "address": str,
                "chain": str,
                "entity_id": str | None,
                "entity_label": str,
                "entity_category": str,
                "linked_wallets": [...],
                "risk_score": float (0-100),
                "risk_level": str,
                "scam_associations": [...],
                "deployer_history": [...],
                "cross_chain_presence": [...],
                "labels": [...],
                "confidence": float,
            }
        """
        addr = address.lower().strip()
        result: dict[str, Any] = {
            "address": addr,
            "chain": chain,
            "entity_id": None,
            "entity_label": "",
            "entity_category": "unknown",
            "linked_wallets": [],
            "risk_score": 0.0,
            "risk_level": "unknown",
            "scam_associations": [],
            "deployer_history": [],
            "cross_chain_presence": [],
            "labels": [],
            "confidence": 0.0,
        }

        # 1. Labels (from 6 live sources + static databases)
        try:
            label_report = await self.labels.resolve(addr, chain)
            if label_report:
                result["labels"] = [
                    {
                        "source": line.get("source", ""),
                        "label": line.get("label", ""),
                        "category": line.get("category", ""),
                        "confidence": line.get("confidence", 1.0),
                    }
                    for line in label_report.get("labels", [])
                ]
                result["entity_label"] = label_report.get("primary_label", "")
                result["entity_category"] = label_report.get("primary_category", "")
                if label_report.get("is_known_scam"):
                    result["scam_associations"].append(
                        {
                            "source": "address_labeler",
                            "type": "known_scam",
                            "label": label_report.get("primary_label", ""),
                        }
                    )
                if label_report.get("is_sanctioned"):
                    result["scam_associations"].append(
                        {
                            "source": "ofac",
                            "type": "sanctioned",
                            "label": "OFAC sanctioned entity",
                        }
                    )
        except Exception as e:
            logger.debug(f"Label resolution failed for {addr}: {e}")

        # 2. Entity from storage (ClickHouse / Redis)
        try:
            entity = await self.storage.get_entity_for_wallet(addr, chain)
            if entity:
                result["entity_id"] = entity.get("entity_id")
                if not result["entity_label"] and entity.get("entity_label"):
                    result["entity_label"] = entity["entity_label"]
                if entity.get("entity_category") and result["entity_category"] == "unknown":
                    result["entity_category"] = entity["entity_category"]
                result["confidence"] = entity.get("confidence_score", 0.0)

                # Get linked wallets in this entity
                if entity.get("entity_id"):
                    linked = await self.storage.get_entity_wallets(entity["entity_id"])
                    result["linked_wallets"] = [
                        {
                            "address": w["wallet_address"],
                            "chain": w["chain_id"],
                            "confidence": w["confidence_score"],
                        }
                        for w in linked
                        if w["wallet_address"].lower() != addr
                    ]
        except Exception as e:
            logger.debug(f"Entity lookup failed for {addr}: {e}")

        # 3. Scam database check
        try:
            scam = await self.storage.check_scam_address(addr, chain)
            if scam:
                result["scam_associations"].append(
                    {
                        "source": scam.get("source", "unknown"),
                        "type": scam.get("threat_type", "unknown"),
                        "confidence": scam.get("confidence", 1.0),
                    }
                )
        except Exception as e:
            logger.debug(f"Scam check failed for {addr}: {e}")

        # 4. Deployer history
        try:
            history = await self.storage.get_deployer_history(addr, chain)
            result["deployer_history"] = history
        except Exception as e:
            logger.debug(f"Deployer history failed for {addr}: {e}")

        # 5. Cross-chain presence (bridge mappings)
        try:
            bridges = await self.storage.get_bridge_links(addr, chain)
            result["cross_chain_presence"] = bridges
        except Exception as e:
            logger.debug(f"Bridge lookup failed for {addr}: {e}")

        # 6. Composite risk score
        try:
            risk = await self.risk_scorer.score(addr, chain, result)
            result["risk_score"] = risk.get("score", 0.0)
            result["risk_level"] = risk.get("level", "unknown")
        except Exception as e:
            logger.debug(f"Risk scoring failed for {addr}: {e}")

        # 7. Cluster from in-memory engine (if data was fed)
        try:
            cluster = self.clustering.get_cluster_for_wallet(addr)
            if cluster and len(cluster.wallets) > 1:
                # Merge cluster members into linked_wallets if not already present
                existing_addrs = {w["address"].lower() for w in result["linked_wallets"]}
                for w in cluster.wallets:
                    if w.lower() != addr and w.lower() not in existing_addrs:
                        result["linked_wallets"].append(
                            {
                                "address": w,
                                "chain": chain,
                                "confidence": round(cluster.confidence, 3),
                                "heuristic": "clustering",
                            }
                        )
                # Boost confidence from multiple detection methods
                if cluster.detection_methods:
                    method_bonus = min(0.2, len(cluster.detection_methods) * 0.05)
                    result["confidence"] = min(0.99, result["confidence"] + method_bonus)
        except Exception as e:
            logger.debug(f"Cluster lookup failed for {addr}: {e}")

        return result

    # ── Primary API: Wallet Profile (WalletSafe frontend calls this) ─

    async def get_wallet_profile(self, address: str, chain: str, depth: int = 1) -> dict[str, Any]:
        """
        Full wallet profile for the WalletSafe product.
        Includes entity info, risk, labels, cluster, and connected wallets.
        Auto-triggers ingestion pipeline when profile is empty or stale.
        """
        addr = address.lower().strip()

        # Check storage first (may have been enriched before)
        stored = await self.storage.get_wallet_profile(addr, chain)

        # ── Auto-ingestion: fetch fresh data if profile is empty or stale ──
        if self.ingestion and self.ingestion.should_ingest(addr, chain, stored):
            try:
                ingest_ok = await self.ingestion.ingest_if_needed(addr, chain)
                if ingest_ok:
                    logger.info(f"Ingestion triggered for {addr} on {chain}")
                    # Re-fetch stored profile after ingestion may have updated it
                    stored = await self.storage.get_wallet_profile(addr, chain)
            except Exception as e:
                logger.debug(f"Ingestion trigger failed for {addr}: {e}")

        # Build fresh profile
        intel = await self.get_deployer_intelligence(addr, chain)

        profile = {
            "address": addr,
            "chain": chain,
            "entity_id": intel.get("entity_id"),
            "entity_label": intel.get("entity_label"),
            "entity_category": intel.get("entity_category"),
            "risk_score": intel.get("risk_score", 0),
            "risk_level": intel.get("risk_level", "unknown"),
            "labels": intel.get("labels", []),
            "scam_associations": intel.get("scam_associations", []),
            "deployer_history": intel.get("deployer_history", []),
            "cross_chain_presence": intel.get("cross_chain_presence", []),
            "linked_wallets": intel.get("linked_wallets", []),
            "confidence": intel.get("confidence", 0),
            # From stored profile if available
            "total_transactions": stored.get("total_transactions", 0) if stored else 0,
            "total_volume": stored.get("total_volume", 0) if stored else 0,
            "first_seen": stored.get("first_seen_at") if stored else None,
            "last_seen": stored.get("last_seen_at") if stored else None,
        }

        # Save/refresh the profile
        try:
            await self._persist_profile(profile)
        except Exception as e:
            logger.debug(f"Profile persist failed: {e}")

        return profile

    # ── Data flywheel: flag deployer from scan results ────────────

    async def flag_deployer(
        self,
        address: str,
        chain: str,
        reason: str = "high_risk_token",
        token: str = "",
        score: float = 0.0,
    ) -> bool:
        """
        Feed scan results back into the Wallet Memory Bank.
        This is the data flywheel: scans produce intelligence that makes future scans better.
        """
        addr = address.lower().strip()

        # Save scam address if high risk
        if score >= 70:
            try:
                await self.storage.save_scam_address(
                    address=addr,
                    chain_id=chain,
                    source="sentinel_scan",
                    threat_type=reason,
                    confidence=round(min(1.0, score / 100), 2),
                    evidence=f"token={token},risk_score={score:.1f}",
                )
            except Exception as e:
                logger.warning(f"Failed to flag deployer {addr}: {e}")

        # Record the deployment event
        if token:
            try:
                is_scam = score >= 70
                await self.storage.save_deployer_event(
                    deployer=addr,
                    chain_id=chain,
                    token_address=token,
                    outcome="flagged" if is_scam else "unknown",
                    is_scam=is_scam,
                    risk_score=round(score / 100, 2),
                    deployed_at=datetime.now(UTC),
                )
            except Exception as e:
                logger.warning(f"Failed to save deployer event for {addr}: {e}")

        # Update the wallet profile with new risk info
        try:
            stored = await self.storage.get_wallet_profile(addr, chain) or {}
            new_risk = max(stored.get("risk_score", 0) or 0, score)
            await self.storage.save_wallet_profile(
                {
                    "wallet_address": addr,
                    "chain_id": chain,
                    "risk_score": round(new_risk / 100, 2),
                    "risk_level": _risk_level(new_risk),
                    "entity_id": stored.get("entity_id", ""),
                    "entity_label": stored.get("entity_label", ""),
                    "entity_category": stored.get("entity_category", "unknown"),
                    "total_transactions": stored.get("total_transactions", 0),
                    "total_volume": stored.get("total_volume", 0),
                    "unique_counterparties": stored.get("unique_counterparties", 0),
                    "first_seen_at": stored.get("first_seen_at", datetime.now(UTC)),
                    "last_seen_at": datetime.now(UTC),
                }
            )
        except Exception as e:
            logger.debug(f"Profile update after flag failed: {e}")

        logger.info(f"Flagged {addr} on {chain}: reason={reason}, score={score:.1f}")
        return True

    # ── Search across all wallets ────────────────────────────────

    async def search_wallets(self, query: str, chain: str = "", limit: int = 20) -> list[dict]:
        """
        Search for wallets by address prefix, label, or entity name.
        Uses Redis search first, then ClickHouse if available.
        """
        results = []
        q = query.lower().strip()

        # Try Redis key prefix scan
        try:
            redis = self.storage._redis
            if not redis:
                await self.storage._ensure_connections()
                redis = self.storage._redis

            if redis:
                # Scan profile keys
                pattern = f"wm:profile:*{q}*"
                async for key in redis.scan_iter(match=pattern, count=100):
                    cached = await redis.get(key)
                    if cached:
                        import json

                        profile = json.loads(cached)
                        if len(results) < limit:
                            results.append(profile)
        except Exception as e:
            logger.debug(f"Redis search failed: {e}")

        return results[:limit]

    # ── Feed transactions into clustering engine ─────────────────

    def feed_transactions(self, transactions: list[dict], chain_id: str = ""):
        """
        Feed transaction data into the in-memory clustering engine.
        Call this when pulling data from Helius/Etherscan for a wallet.

        Expected transaction dict keys:
          address, counterparty, timestamp (datetime or ISO string), amount, program
        """
        for tx in transactions:
            addr = tx.get("address", tx.get("from", "")).lower()
            cp = tx.get("counterparty", tx.get("to", "")).lower()
            ts = tx.get("timestamp")
            if isinstance(ts, str):
                from datetime import datetime as dt

                ts = dt.fromisoformat(ts.replace("Z", "+00:00"))
            elif not isinstance(ts, datetime):
                ts = datetime.now(UTC)

            self.clustering.add_transaction(
                address=addr,
                counterparty=cp,
                timestamp=ts,
                amount=tx.get("amount", 0),
                chain_id=chain_id,
                program=tx.get("program", ""),
            )

    # ── Persist clusters to storage ──────────────────────────────

    async def persist_clusters(self) -> int:
        """Run all heuristics and persist resulting clusters to ClickHouse/Redis."""
        clusters = self.clustering.run_all_heuristics()
        persisted = 0

        for cluster in clusters:
            for wallet in cluster.wallets:
                try:
                    await self.storage.save_entity_link(
                        entity_id=cluster.cluster_id,
                        wallet_address=wallet,
                        chain_id="",  # Cross-chain cluster
                        heuristic_type=",".join(cluster.detection_methods),
                        confidence=cluster.confidence,
                        label=cluster.label,
                        category=cluster.category,
                    )
                    persisted += 1
                except Exception as e:
                    logger.debug(f"Entity link persist failed for {wallet}: {e}")

        logger.info(f"Persisted {persisted} entity links from {len(clusters)} clusters")
        return persisted

    # ── Internal ─────────────────────────────────────────────────

    async def _persist_profile(self, profile: dict):
        """Save a wallet profile to storage."""
        await self.storage.save_wallet_profile(
            {
                "wallet_address": profile["address"],
                "chain_id": profile["chain"],
                "entity_id": profile.get("entity_id", ""),
                "entity_label": profile.get("entity_label", ""),
                "entity_category": profile.get("entity_category", "unknown"),
                "risk_score": profile.get("risk_score", 0),
                "risk_level": profile.get("risk_level", "unknown"),
                "total_transactions": profile.get("total_transactions", 0),
                "total_volume": profile.get("total_volume", 0),
                "unique_counterparties": 0,
                "first_seen_at": profile.get("first_seen_at") or datetime.now(UTC),
                "last_seen_at": datetime.now(UTC),
            }
        )


# ── Risk level helper ───────────────────────────────────────────


def _risk_level(score: float) -> str:
    if score >= 80:
        return "critical"
    elif score >= 60:
        return "high"
    elif score >= 40:
        return "medium"
    elif score >= 20:
        return "low"
    return "minimal"


# ── Global singleton ────────────────────────────────────────────

_engine: WalletMemoryEngine | None = None


def get_wallet_engine() -> WalletMemoryEngine:
    """Get global wallet memory engine instance."""
    global _engine
    if _engine is None:
        _engine = WalletMemoryEngine()
    return _engine
