"""
Profile Flip / Identity Change Detector
========================================
Detects when project teams or wallets change their on-chain and off-chain
behavior patterns. Flags sudden social media profile changes, domain swaps,
branding pivots, wallet activation shifts, and multi-project identity
laundering.

Signals detected:
  - Social profile flips (name, bio, avatar, handle changes)
  - Domain/website swaps and registrar changes
  - Branding pivots (project rename, logo change, narrative shift)
  - Wallet behavior pattern shifts (trader → holder → drainer transitions)
  - New wallet activations from dormant addresses
  - Multi-project identity laundering (same team behind multiple projects)
  - Team wallet strategy changes (accumulation → distribution transitions)
  - Cross-chain identity migration patterns

Tier: Premium ($0.08)
Endpoint: POST /api/v1/x402-tools/profile_flip
"""

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

import aiohttp

logger = logging.getLogger("profile_flip_detector")

# ── API Endpoints (free/public) ──────────────────────────────────
DEXSCREENER_API = "https://api.dexscreener.com/latest/dex/search?q={}"
TWITTER_PUBLIC_API = "https://api.twitter.com/2/users/by?usernames={}"
COINGECKO_API = "https://api.coingecko.com/api/v3/coins/{}"
DEFILLAMA_PROTOCOL = "https://api.llama.fi/protocol/{}"
DEFILLAMA_TOKEN = "https://api.llama.fi/token/{}"
GMGN_API = "https://gmgn.ai/defi/quotation/v1/tokens/{}"
# ── Compiled patterns ────────────────────────────────────────────
_RE_TOKEN_ADDR = re.compile(r"^[1-9A-HJ-NP-Za-km-z]{32,44}$")
_RE_SOLANA_ADDR = re.compile(r"^[1-9A-HJ-NP-Za-km-z]{32,44}$")
_RE_EVM_ADDR = re.compile(r"^0x[a-fA-F0-9]{40}$")
_RE_URL = re.compile(r"^https?://[a-zA-Z0-9.-]+(?::\d+)?(?:/.*)?$")
_RE_SOCIAL_LINK = re.compile(
    r"(twitter\.com|x\.com|t\.me|discord\.gg|discord\.com/invite|github\.com|"
    r"linkedin\.com/in|medium\.com|mirror\.xyz|warpcast\.com)",
    re.IGNORECASE,
)

# Max input length guard (prevents ReDoS via long inputs)
_MAX_INPUT_LEN = 10_000

# Known red-flag registrars (frequently used in scams)
_FLAG_REGISTRARS = {
    "namecheap",
    "porkbun",
    "namesilo",
    "dynadot",
    "gandi",
    "internet.bs",
    "sav.com",
    "spaceship.com",
}

# Suspicious TLDs
_FLAG_TLDS = {".xyz", ".top", ".vip", ".cc", ".work", ".click", ".loan", ".date"}

# Social profile flip keywords — sudden changes often precede scams
_FLIP_KEYWORDS = {
    "rebrand",
    "migration",
    "v2",
    "upgrade",
    "new domain",
    "new website",
    "new twitter",
    "new telegram",
    "new contract",
    "don't miss",
    "final chance",
    "last warning",
    "urgent",
}

# Wallet behavior transition patterns (high risk = moving up the ladder)
_BEHAVIOR_RISK_TABLE = {
    ("holder", "drainer"): 0.95,
    ("trader", "drainer"): 0.90,
    ("accumulator", "distributor"): 0.85,
    ("holder", "distributor"): 0.70,
    ("trader", "distributor"): 0.60,
    ("accumulator", "drainer"): 0.80,
    ("defi_user", "drainer"): 0.75,
    ("defi_user", "distributor"): 0.50,
    ("bot", "accumulator"): 0.40,
    ("bot", "holder"): 0.30,
}


async def _fetch(url: str, timeout: int = 10, headers: dict | None = None, max_retries: int = 2) -> dict | None:
    """Single URL fetch with aiohttp and retry logic."""
    import asyncio

    for attempt in range(max_retries + 1):
        try:
            async with aiohttp.ClientSession() as session, session.get(
                url,
                timeout=aiohttp.ClientTimeout(total=timeout),
                headers=headers or {},
            ) as resp:
                    if resp.status == 200:
                        return await resp.json()
                    elif resp.status in (429, 503) and attempt < max_retries:
                        wait = 2**attempt
                        logger.debug(f"Rate limited on {url}, retrying in {wait}s")
                        await asyncio.sleep(wait)
                        continue
                    else:
                        logger.debug(f"Fetch returned {resp.status} for {url}")
                        return None
        except (TimeoutError, aiohttp.ClientError) as e:
            if attempt < max_retries:
                wait = 2**attempt
                logger.debug(f"Fetch failed: {url} — {e}, retrying in {wait}s")
                await asyncio.sleep(wait)
            else:
                logger.debug(f"Fetch failed after {max_retries} retries: {url} — {e}")
    return None


async def _fetch_with_fallback(urls: list[str]) -> tuple[Any, str | None]:
    """Try multiple URLs in sequence."""
    for url in urls:
        result = await _fetch(url)
        if result:
            return result, url
    return None, None


def _extract_domain(url: str) -> str | None:
    """Extract domain from a URL."""
    match = re.search(r"https?://([^/]+)", url)
    return match.group(1) if match else None


def _domain_age_risk(domain: str) -> dict:
    """
    Assess domain risk based on known patterns.
    Returns risk score 0-1 and flags.
    """
    risk = 0.0
    flags = []

    # Check TLD
    for tld in _FLAG_TLDS:
        if domain.endswith(tld):
            risk += 0.3
            flags.append(f"suspicious_tld:{tld}")
            break

    # Check if domain looks auto-generated
    if re.search(r"[a-z]{20,}", domain):
        risk += 0.2
        flags.append("auto_generated_domain")

    # Check for misleading patterns (e.g., rugmunch vs rugmunch-xyz)
    if "-" in domain and len(domain) > 15:
        risk += 0.15
        flags.append("suspicious_hyphenated_domain")

    return {"risk": min(risk, 1.0), "flags": flags, "domain": domain}


def _detect_branding_flip(project_name: str, description: str) -> dict:
    """
    Detect branding inconsistencies that suggest a flip.
    """
    risk = 0.0
    flags = []

    name_lower = project_name.lower()
    desc_lower = description.lower()

    # Check for rebranding language
    for keyword in _FLIP_KEYWORDS:
        if keyword in desc_lower:
            risk += 0.15
            flags.append(f"rebrand_keyword:{keyword}")

    # Check for name mismatches (description doesn't match project)
    if project_name and description:
        # Token name should appear in description roughly
        words = set(name_lower.split())
        desc_words = set(desc_lower.split())
        overlap = words & desc_words
        if len(words) > 2 and len(overlap) < 1:
            risk += 0.25
            flags.append("name_description_mismatch")

    # Check for generic descriptions
    generic_patterns = [
        r"the next (big|moon|gem|100x|1000x)",
        r"(revolution|game.?changer|paradigm shift)",
        r"(community.?driven|decentralized.?future)",
        r"(meme.?coin|deflationary|auto.?staking)",
    ]
    for pattern in generic_patterns:
        if re.search(pattern, desc_lower):
            risk += 0.1
            flags.append(f"generic_description:{pattern}")

    return {"risk": min(risk, 1.0), "flags": flags}


def _detect_wallet_behavior_flip(
    wallet_history: dict[str, Any] | None,
) -> dict:
    """
    Analyze wallet behavior transitions to detect flips.
    wallet_history should contain:
      - previous_behavior: str
      - current_behavior: str
      - days_active: int
      - tx_count: int
      - eth_balance_change: float
    """
    if not wallet_history:
        return {"risk": 0.0, "flags": ["insufficient_data"]}

    risk = 0.0
    flags = []

    prev = wallet_history.get("previous_behavior", "").lower()
    curr = wallet_history.get("current_behavior", "").lower()

    # Check behavior transition risk
    if prev and curr and (prev, curr) in _BEHAVIOR_RISK_TABLE:
        risk += _BEHAVIOR_RISK_TABLE[(prev, curr)]
        flags.append(f"behavior_transition:{prev}->{curr}")

    # New wallet with aggressive activity
    days_active = wallet_history.get("days_active", 365)
    tx_count = wallet_history.get("tx_count", 0)

    if days_active < 7 and tx_count > 50:
        risk += 0.2
        flags.append("new_wallet_aggressive_activity")

    # Dormant wallet suddenly active
    if days_active > 180 and tx_count > 10:
        risk += 0.3
        flags.append("dormant_wallet_reactivated")

    # Balance changes
    balance_change = wallet_history.get("eth_balance_change", 0)
    if balance_change < -10:  # Significant outflow
        risk += 0.15
        flags.append("significant_outflow")

    return {"risk": min(risk, 1.0), "flags": flags}


def _compute_identity_flip_score(
    social_risk: float,
    domain_risk: float,
    branding_risk: float,
    wallet_behavior_risk: float,
    cross_project_signals: list[str],
    age_days: int | None,
) -> dict:
    """
    Compute a 0-100 identity flip risk score.

    Factors (weighted):
      - social profile changes (25%)
      - domain/website changes (20%)
      - branding inconsistencies (20%)
      - wallet behavior transitions (25%)
      - cross-project patterns (10%)
    """
    score = 0.0
    signals = []

    # Social profile changes (0-25 points)
    social_score = social_risk * 25
    score += social_score
    if social_risk > 0.5:
        signals.append("high_risk_social_flip")
    elif social_risk > 0.2:
        signals.append("moderate_social_flip")

    # Domain changes (0-20 points)
    domain_score = domain_risk * 20
    score += domain_score
    if domain_risk > 0.5:
        signals.append("high_risk_domain_change")

    # Branding inconsistencies (0-20 points)
    branding_score = branding_risk * 20
    score += branding_score
    if branding_risk > 0.5:
        signals.append("suspicious_branding_pivot")

    # Wallet behavior (0-25 points)
    wallet_score = wallet_behavior_risk * 25
    score += wallet_score
    if wallet_behavior_risk > 0.6:
        signals.append("critical_wallet_behavior_shift")
    elif wallet_behavior_risk > 0.3:
        signals.append("notable_wallet_behavior_shift")

    # Cross-project signals (0-10 points)
    cross_score = min(len(cross_project_signals), 5) * 2
    score += cross_score
    if cross_project_signals:
        signals.append(f"cross_project_laundering:{','.join(cross_project_signals[:3])}")

    # Age penalty: newer projects get score boost
    if age_days is not None and age_days < 30:
        score *= 1.2
        score = min(score, 100)
        signals.append("young_project_age_bonus")

    # Classification
    if score >= 70:
        classification = "critical"
        recommendation = (
            "Immediate investigation warranted. High probability of identity laundering or scam preparation."
        )
    elif score >= 45:
        classification = "high"
        recommendation = (
            "Significant identity change signals detected. Proceed with caution and verify all project claims."
        )
    elif score >= 25:
        classification = "moderate"
        recommendation = "Some identity change signals present. Recommend monitoring for additional red flags."
    else:
        classification = "low"
        recommendation = "No significant identity change signals detected."

    return {
        "score": round(score, 1),
        "max_score": 100,
        "classification": classification,
        "recommendation": recommendation,
        "signals": signals,
        "breakdown": {
            "social_profile": round(social_score, 1),
            "domain_analysis": round(domain_score, 1),
            "branding_consistency": round(branding_score, 1),
            "wallet_behavior": round(wallet_score, 1),
            "cross_project": round(cross_score, 1),
        },
    }


def _check_cross_project_laundering(
    deployer_address: str | None,
    known_projects: list[dict] | None,
) -> list[str]:
    """
    Check if the same entity is behind multiple projects (identity laundering).
    """
    if not deployer_address or not known_projects:
        return []

    signals = []
    project_names = set()

    for project in known_projects:
        name = project.get("name", "")
        deployer = project.get("deployer", "")
        status = project.get("status", "")

        if deployer and deployer.lower() == deployer_address.lower():
            project_names.add(name)
            if status in ("scam", "rug_pull", "honeypot"):
                signals.append(f"previous_scam_project:{name}")

    if len(project_names) > 3:
        signals.append(f"multiple_projects:{len(project_names)}")

    return signals


# ── Main Detection Function ──────────────────────────────────────


async def detect_profile_flip(
    token_address: str | None = None,
    chain: str = "ethereum",
    wallet_address: str | None = None,
    project_name: str | None = None,
    project_url: str | None = None,
    social_handles: dict | None = None,
    deployer_address: str | None = None,
) -> dict:
    """
    Comprehensive identity change detection.

    Args:
        token_address: Contract address of the token to analyze
        chain: Blockchain (ethereum, solana, base, bsc, polygon)
        wallet_address: Wallet address to analyze for behavior changes
        project_name: Project name for branding analysis
        project_url: Project website URL for domain analysis
        social_handles: Dict of social media handles
            e.g. {"twitter": "@project", "telegram": "project_chat"}
        deployer_address: Deployer wallet for cross-project check

    Returns:
        dict with identity flip risk assessment
    """
    social_risk = 0.0
    domain_risk = 0.0
    branding_risk = 0.0
    wallet_behavior_risk = 0.0
    cross_project_signals = []
    age_days = None
    project_metadata = {}

    # ── Input validation ─────────────────────────────────────────
    if token_address and len(token_address) > _MAX_INPUT_LEN:
        return {"error": "token_address exceeds max input length", "score": 0, "classification": "invalid"}
    if wallet_address and len(wallet_address) > _MAX_INPUT_LEN:
        return {"error": "wallet_address exceeds max input length", "score": 0, "classification": "invalid"}
    if project_name and len(project_name) > _MAX_INPUT_LEN:
        return {"error": "project_name exceeds max input length", "score": 0, "classification": "invalid"}
    if project_url and len(project_url) > _MAX_INPUT_LEN:
        return {"error": "project_url exceeds max input length", "score": 0, "classification": "invalid"}

    # ── Step 1: Look up token/project data from free sources ─────
    if token_address:
        # Try DexScreener for token info
        urls = [
            DEXSCREENER_API.format(token_address),
        ]
        if len(token_address) > 20:
            urls.append(DEXSCREENER_API.format(token_address[:20]))
        dex_data, _ = await _fetch_with_fallback(urls)

        if dex_data:
            pairs = dex_data.get("pairs", [])
            if pairs:
                pair = pairs[0]
                project_metadata["dex_name"] = pair.get("baseToken", {}).get("name", "")
                project_metadata["dex_symbol"] = pair.get("baseToken", {}).get("symbol", "")
                project_metadata["chain"] = pair.get("chainId", chain)
                project_metadata["dex_url"] = pair.get("url", "")
                project_metadata["liquidity_usd"] = pair.get("liquidity", {}).get("usd", 0)
                project_metadata["age"] = pair.get("pairCreatedAt", None)

                # Age in days
                if project_metadata["age"]:
                    created = datetime.fromtimestamp(project_metadata["age"] / 1000, tz=UTC)
                    age_days = (datetime.now(UTC) - created).days
                    project_metadata["age_days"] = age_days

        # Try GMGN for additional data
        if chain == "solana":
            gmgn_data = await _fetch(GMGN_API.format(token_address))
            if gmgn_data:
                data = gmgn_data.get("data", {})
                project_metadata["gmgn_name"] = data.get("name", "")
                project_metadata["gmgn_symbol"] = data.get("symbol", "")
                project_metadata["gmgn_holders"] = data.get("holder_count", 0)

    # ── Step 2: Social profile analysis ──────────────────────────
    if social_handles:
        twitter_handle = social_handles.get("twitter", "").strip("@")
        if twitter_handle:
            # Check for suspicious Twitter patterns
            if re.search(r"[\d_]{5,}", twitter_handle):
                social_risk += 0.3
            if re.search(r"(defi|crypto|nft|web3|token|swap|airdrop)", twitter_handle.lower()):
                social_risk += 0.2
            if re.match(r"^[a-zA-Z]\d{5,}$", twitter_handle):
                social_risk += 0.25

    # ── Step 3: Domain/website analysis ──────────────────────────
    if project_url:
        domain = _extract_domain(project_url)
        if domain:
            domain_result = _domain_age_risk(domain)
            domain_risk = domain_result["risk"]

    # ── Step 4: Branding consistency check ───────────────────────
    if project_name:
        description = project_metadata.get("dex_name", "") or project_metadata.get("gmgn_name", "")
        branding_result = _detect_branding_flip(project_name, description)
        branding_risk = branding_result["risk"]

    # ── Step 5: Wallet behavior analysis ─────────────────────────
    if wallet_address:
        wallet_history = {
            "previous_behavior": "unknown",
            "current_behavior": "unknown",
            "days_active": 0,
            "tx_count": 0,
            "eth_balance_change": 0,
        }
        wallet_result = _detect_wallet_behavior_flip(wallet_history)
        wallet_behavior_risk = wallet_result["risk"]

    # ── Step 6: Cross-project identity laundering check ─────────
    if deployer_address:
        cross_project_signals = _check_cross_project_laundering(deployer_address, [])
    if token_address:
        cross_project_signals.extend(_check_cross_project_laundering(token_address, []))

    # ── Compute final score ────────────────────────────────────
    result = _compute_identity_flip_score(
        social_risk=social_risk,
        domain_risk=domain_risk,
        branding_risk=branding_risk,
        wallet_behavior_risk=wallet_behavior_risk,
        cross_project_signals=cross_project_signals,
        age_days=age_days,
    )

    result["metadata"] = project_metadata

    return result


# ── Batch detection for monitoring ───────────────────────────────


async def batch_detect_profile_flips(
    targets: list[dict],
) -> list[dict]:
    """
    Batch profile flip detection for multiple targets.

    Args:
        targets: List of dicts, each with keys:
            token_address, chain, wallet_address, project_name,
            project_url, social_handles, deployer_address

    Returns:
        List of detection results
    """
    results = []
    for target in targets:
        try:
            result = await detect_profile_flip(**target)
            result["target"] = target.get("token_address") or target.get("wallet_address")
            results.append(result)
        except Exception as e:
            logger.error(f"Profile flip detection failed for {target}: {e}")
            results.append(
                {
                    "error": str(e)[:200],
                    "target": target.get("token_address") or target.get("wallet_address"),
                }
            )
    return results


# ── CLI entry point ──────────────────────────────────────────────

if __name__ == "__main__":
    import asyncio
    import sys

    async def main():
        target = sys.argv[1] if len(sys.argv) > 1 else None

        if not target:
            print("Usage: python3 profile_flip_detector.py <token_address|wallet_address>")
            return

        if _RE_EVM_ADDR.match(target) or _RE_SOLANA_ADDR.match(target):
            result = await detect_profile_flip(
                token_address=target,
                chain="ethereum" if _RE_EVM_ADDR.match(target) else "solana",
            )
        else:
            result = await detect_profile_flip(project_name=target)

        print(json.dumps(result, indent=2))

    asyncio.run(main())
