"""
Contract Upgrade Monitor
========================
Monitor proxy contract upgrades in real-time - detects malicious implementation
swaps, hidden timelock changes, and privilege escalation through upgrade patterns.

TOOL  : contract_upgrade_monitor
TIER  : security
PRICE : $0.05 (50000 atoms)
TRIAL : 2 free checks

Data Sources (all free-tier):
- Etherscan/BscScan/Polygonscan API - contract ABI, source code, tx history
- OpenZeppelin proxy patterns (UUPS, Transparent, Beacon)
- Public RPCs - storage slot reads for implementation address
- EIP-1967, EIP-1822, EIP-1167 storage slot standards
"""

from __future__ import annotations

import asyncio
import json
import logging
import os
import re
import time
from dataclasses import dataclass, field
from typing import Any

import httpx

logger = logging.getLogger(__name__)

# ── Constants ────────────────────────────────────────────────────

# EIP-1967 proxy storage slots
EIP1967_IMPLEMENTATION_SLOT = "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc"
EIP1967_ADMIN_SLOT = "0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103"
EIP1967_BEACON_SLOT = "0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50"

# EIP-1822 (UUPS) storage slot
EIP1822_IMPLEMENTATION_SLOT = "0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7"

BEACON_IMPLEMENTATION_SLOT = "0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50"

ETHERSCAN_BASES: dict[str, str] = {
    "ethereum": "https://api.etherscan.io/api",
    "bsc": "https://api.bscscan.com/api",
    "polygon": "https://api.polygonscan.com/api",
    "arbitrum": "https://api.arbiscan.io/api",
    "optimism": "https://api-optimistic.etherscan.io/api",
    "base": "https://api.basescan.org/api",
    "avalanche": "https://api.snowtrace.io/api",
}

# Supported proxy patterns
PROXY_PATTERNS = {
    "eip1967": "EIP-1967 Transparent/Universal Proxy",
    "eip1822": "EIP-1822 UUPS Proxy",
    "beacon": "EIP-1967 Beacon Proxy",
    "gnosis_safe": "Gnosis Safe Proxy",
    "openzeppelin": "OpenZeppelin Proxy (legacy)",
    "eip1167": "EIP-1167 Minimal Proxy (clone)",
}

# Dangerous upgrade-related function selectors (4-byte signatures)
DANGEROUS_SELECTORS = {
    "0x3659cfe6": "upgradeTo(address)",
    "0x4f1ef286": "upgradeToAndCall(address,bytes)",
    "0x9623609d": "_upgradeTo(address)",
    "0x99a88ec4": "upgrade(address)",
    "0x077f224a": "upgradeImplementation(address)",
    "0x2e4a7ec4": "setImplementation(address)",
    "0x8eaa6ac0": "changeAdmin(address)",
    "0xf851a440": "owner()",
    "0x8da5cb5b": "owner() [alternative]",
    "0x5c60da1b": "implementation()",
    "0xbb20d857": "upgradeToAndCall(address,bytes)",
    "0x1a8450c0": "setAddress(address)",
    "0xcb13bd91": "setBeacon(address)",
    "0x9b3b76cc": "upgradeBeaconTo(address,bytes)",
}

# Proxy storage diff threshold - if implementation changes within this window
SUSPICIOUS_UPGRADE_WINDOW = 86400  # 24 hours

# Cache helpers
_cache: dict[str, tuple[float, Any]] = {}
_CACHE_TTL = 300  # 5 minutes
_MAX_STALE_TTL = 1800  # 30 minutes


# ── Data Models ───────────────────────────────────────────────────


@dataclass
class ProxyInfo:
    """Detected proxy pattern and current implementation."""

    address: str
    chain: str
    proxy_type: str | None = None
    proxy_type_name: str | None = None
    implementation_address: str | None = None
    admin_address: str | None = None
    beacon_address: str | None = None
    is_proxy: bool = False
    confidence: float = 0.0  # 0.0 - 1.0
    detected_selectors: list[str] = field(default_factory=list)


@dataclass
class UpgradeEvent:
    """Record of a proxy upgrade event."""

    block_number: int
    transaction_hash: str
    timestamp: int
    previous_implementation: str | None = None
    new_implementation: str | None = None
    triggered_by: str | None = None  # address that triggered the upgrade
    function_selector: str | None = None


@dataclass
class ContractUpgradeReport:
    """Complete upgrade monitoring report for a contract."""

    contract_address: str
    chain: str
    proxy_info: ProxyInfo | None = None
    upgrade_history: list[UpgradeEvent] = field(default_factory=list)
    recent_suspicious_upgrades: list[UpgradeEvent] = field(default_factory=list)
    timelock_status: str | None = None  # "active", "bypassed", "none"
    risk_score: float = 0.0  # 0-100
    risk_factors: list[str] = field(default_factory=list)
    admin_privileges: list[str] = field(default_factory=list)
    upgrade_count_30d: int = 0
    last_upgrade_time: int | None = None
    implementation_age_days: int | None = None
    summary: str = ""


# ── Address Validation ────────────────────────────────────────────

EVM_ADDRESS_RE = re.compile(r"^0x[a-fA-F0-9]{40}$")


def is_valid_evm_address(address: str) -> bool:
    """Validate an EVM address format with optional EIP-55 checksum."""
    return bool(EVM_ADDRESS_RE.match(address))


# ── HTTP Client ───────────────────────────────────────────────────


async def _fetch_url(url: str, headers: dict | None = None, timeout: float = 15.0) -> dict | None:
    """Fetch JSON from a URL with caching."""
    cache_key = f"fetch:{url}"
    now = time.time()

    # Check cache
    if cache_key in _cache:
        cached_time, cached_data = _cache[cache_key]
        if now - cached_time < _CACHE_TTL:
            return cached_data
        if now - cached_time > _MAX_STALE_TTL:
            del _cache[cache_key]
        else:
            # Return stale, refresh in background
            asyncio.ensure_future(_background_refresh(url, headers, timeout, cache_key))

    try:
        async with httpx.AsyncClient(timeout=timeout) as client:
            resp = await client.get(url, headers=headers or {})
            if resp.status_code == 200:
                data = resp.json()
                _cache[cache_key] = (now, data)
                return data
            elif resp.status_code == 429:
                logger.warning("Rate limited on %s", url[:50])
                return _cache.get(cache_key, (0, None))[1]
            else:
                logger.warning("HTTP %s for %s", resp.status_code, url[:50])
                return None
    except httpx.TimeoutException:
        logger.warning("Timeout fetching %s", url[:50])
        return _cache.get(cache_key, (0, None))[1]
    except Exception as e:
        logger.debug("Fetch error for %s: %s", url[:50], e)
        return _cache.get(cache_key, (0, None))[1]


async def _background_refresh(url: str, headers: dict | None, timeout: float, cache_key: str) -> None:
    """Refresh cache in background so stale data serves immediately."""
    try:
        async with httpx.AsyncClient(timeout=timeout) as client:
            resp = await client.get(url, headers=headers or {})
            if resp.status_code == 200:
                _cache[cache_key] = (time.time(), resp.json())
    except Exception:
        pass


# ── RPC Storage Slot Reader ──────────────────────────────────────


async def read_storage_slot(address: str, slot: str, chain: str, client: httpx.AsyncClient | None = None) -> str | None:
    """Read an EVM storage slot using a public RPC endpoint.

    Uses chain-specific public RPCs. Returns the 32-byte hex value.
    """
    rpc_urls = {
        "ethereum": os.getenv("ETH_RPC", "https://ethereum-rpc.publicnode.com"),
        "bsc": os.getenv("BSC_RPC", "https://bsc-rpc.publicnode.com"),
        "polygon": os.getenv("POLYGON_RPC", "https://polygon-rpc.publicnode.com"),
        "arbitrum": os.getenv("ARBITRUM_RPC", "https://arbitrum-rpc.publicnode.com"),
        "optimism": os.getenv("OPTIMISM_RPC", "https://optimism-rpc.publicnode.com"),
        "base": os.getenv("BASE_RPC", "https://base-rpc.publicnode.com"),
        "avalanche": os.getenv("AVALANCHE_RPC", "https://avalanche-rpc.publicnode.com"),
    }

    rpc_url = rpc_urls.get(chain)
    if not rpc_url:
        logger.warning("No RPC URL for chain %s", chain)
        return None

    payload = {
        "jsonrpc": "2.0",
        "method": "eth_getStorageAt",
        "params": [address, slot, "latest"],
        "id": 1,
    }

    try:
        close_client = client is None
        c = client or httpx.AsyncClient(timeout=10.0)
        if close_client:
            await c.__aenter__()

        try:
            resp = await c.post(
                rpc_url,
                json=payload,
                headers={"Content-Type": "application/json"},
            )
            if resp.status_code == 200:
                result = resp.json()
                value = result.get("result", "0x0000000000000000000000000000000000000000000000000000000000000000")
                # Extract address from padded 32 bytes (last 20 bytes = 40 hex chars)
                if value and len(value) >= 66:
                    addr = "0x" + value[-40:]
                    if addr != "0x0000000000000000000000000000000000000000":
                        return addr
            return None
        finally:
            if close_client:
                await c.__aexit__(None, None, None)
    except Exception as e:
        logger.debug("RPC read failed for %s slot %s: %s", address[:10], slot[:10], e)
        return None


# ── Proxy Detection ──────────────────────────────────────────────


async def detect_proxy(address: str, chain: str, client: httpx.AsyncClient | None = None) -> ProxyInfo:
    """Detect if an address is a proxy contract and identify its type.

    Checks multiple proxy standards in order:
    1. EIP-1967 Transparent/Universal Proxy
    2. EIP-1822 UUPS Proxy
    3. EIP-1967 Beacon Proxy
    4. Gnosis Safe (check storage slot)
    5. Function selector sniffing (if source code available)
    """
    info = ProxyInfo(address=address, chain=chain)
    close_client = client is None
    c = client or httpx.AsyncClient(timeout=15.0)
    if close_client:
        await c.__aenter__()

    try:
        # 1. Check EIP-1967 implementation slot
        impl = await read_storage_slot(address, EIP1967_IMPLEMENTATION_SLOT, chain, c)
        if impl and is_valid_evm_address(impl):
            info.implementation_address = impl
            info.is_proxy = True
            info.confidence = 0.9

            # Check admin slot
            admin = await read_storage_slot(address, EIP1967_ADMIN_SLOT, chain, c)
            if admin and is_valid_evm_address(admin):
                info.admin_address = admin

            # Check beacon slot
            beacon = await read_storage_slot(address, EIP1967_BEACON_SLOT, chain, c)
            if beacon and is_valid_evm_address(beacon):
                info.beacon_address = beacon
                info.proxy_type = "beacon"
                info.proxy_type_name = PROXY_PATTERNS["beacon"]
            else:
                info.proxy_type = "eip1967"
                info.proxy_type_name = PROXY_PATTERNS["eip1967"]
            return info

        # 2. Check EIP-1822 UUPS slot
        impl = await read_storage_slot(address, EIP1822_IMPLEMENTATION_SLOT, chain, c)
        if impl and is_valid_evm_address(impl):
            info.implementation_address = impl
            info.is_proxy = True
            info.proxy_type = "eip1822"
            info.proxy_type_name = PROXY_PATTERNS["eip1822"]
            info.confidence = 0.85
            return info

        # 3. Check for Gnosis Safe proxy pattern
        # Gnosis Safe stores master copy at slot 0
        safe_slot = await read_storage_slot(
            address, "0x0000000000000000000000000000000000000000000000000000000000000000", chain, c
        )
        if safe_slot and is_valid_evm_address(safe_slot) and safe_slot != address:
            # Additional check: Gnosis Safe has specific bytecode pattern
            info.is_proxy = True
            info.implementation_address = safe_slot
            info.proxy_type = "gnosis_safe"
            info.proxy_type_name = PROXY_PATTERNS["gnosis_safe"]
            info.confidence = 0.6

        # 4. Try source code API for function selector detection
        source_info = await _fetch_source_code(address, chain)
        if source_info:
            detected = _detect_proxy_selectors(source_info.get("ABI", ""))
            if detected:
                info.detected_selectors = detected
                if not info.is_proxy:
                    info.is_proxy = True
                    info.proxy_type = "openzeppelin"
                    info.proxy_type_name = PROXY_PATTERNS["openzeppelin"]
                    info.confidence = 0.5

        return info
    finally:
        if close_client:
            await c.__aexit__(None, None, None)


def _detect_proxy_selectors(abi_json: str) -> list[str]:
    """Detect proxy-related function selectors from contract ABI JSON string."""
    detected = []
    try:
        abi = json.loads(abi_json) if isinstance(abi_json, str) else abi_json
        if not isinstance(abi, list):
            return detected
        for item in abi:
            if isinstance(item, dict) and item.get("type") == "function":
                name = item.get("name", "")
                sig = f"{name}({','.join(i.get('type', '') for i in item.get('inputs', []))})"
                # Hash the signature to get 4-byte selector
                import hashlib

                selector = "0x" + hashlib.keccak_256(sig.encode()).hexdigest()[:8]
                if selector in DANGEROUS_SELECTORS:
                    detected.append(selector)
    except (json.JSONDecodeError, Exception):
        pass
    return detected


# ── Source Code Fetcher ──────────────────────────────────────────


async def _fetch_source_code(address: str, chain: str) -> dict | None:
    """Fetch contract source code and ABI from block explorer."""
    base_url = ETHERSCAN_BASES.get(chain)
    if not base_url:
        return None

    # Use free-tier API keys if available, otherwise public endpoints
    api_key = os.getenv(f"{chain.upper()}_ETHERSCAN_KEY", "")

    url = f"{base_url}?module=contract&action=getsourcecode&address={address}"
    if api_key:
        url += f"&apikey={api_key}"

    data = await _fetch_url(url)
    if data and data.get("status") == "1" and data.get("result"):
        result = data["result"]
        if isinstance(result, list) and len(result) > 0:
            return result[0]
        return result
    return None


# ── Upgrade History ──────────────────────────────────────────────


async def fetch_upgrade_history(
    address: str,
    chain: str,
    lookback_blocks: int = 100000,
) -> list[UpgradeEvent]:
    """Fetch proxy upgrade events from block explorer transaction history.

    Looks for 'Upgraded' or 'UpgradeTo' event logs on the proxy contract.
    """
    base_url = ETHERSCAN_BASES.get(chain)
    if not base_url:
        return []

    api_key = os.getenv(f"{chain.upper()}_ETHERSCAN_KEY", "")
    events = []

    # Search for Upgraded event (EIP-1967): topic=0xbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b
    upgrade_topic = "0xbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b"

    url = (
        f"{base_url}?module=logs&action=getLogs"
        f"&address={address}"
        f"&topic0={upgrade_topic}"
        f"&fromBlock=0&toBlock=latest"
        f"&sort=desc"
    )
    if api_key:
        url += f"&apikey={api_key}"

    data = await _fetch_url(url)
    if data and data.get("status") == "1" and data.get("result"):
        for log in data["result"]:
            if isinstance(log, dict):
                upgrade = _parse_upgrade_log(log, address, chain)
                if upgrade:
                    events.append(upgrade)

    # Also check for admin-changing events
    admin_topic = "0xf23ec0bb1e0758b0e3c00d38f1e1c1c0a8f6d4f4b0b8b8c2e4e8f0a0b0c0d0e"
    admin_url = (
        f"{base_url}?module=logs&action=getLogs"
        f"&address={address}"
        f"&topic0={admin_topic}"
        f"&fromBlock=0&toBlock=latest"
        f"&sort=desc"
    )
    if api_key:
        admin_url += f"&apikey={api_key}"

    admin_data = await _fetch_url(admin_url)
    if admin_data and admin_data.get("status") == "1" and admin_data.get("result"):
        for log in admin_data["result"]:
            if isinstance(log, dict):
                logger.debug("Admin event found for %s: %s", address[:10], log.get("transactionHash", "")[:20])

    return events


def _parse_upgrade_log(log: dict, address: str, chain: str) -> UpgradeEvent | None:
    """Parse an upgrade event log entry."""
    try:
        tx_hash = log.get("transactionHash", "")
        block_num = (
            int(log.get("blockNumber", "0"), 16)
            if log.get("blockNumber", "").startswith("0x")
            else int(log.get("blockNumber", 0))
        )
        timestamp = (
            int(log.get("timeStamp", "0"), 16)
            if log.get("timeStamp", "").startswith("0x")
            else int(log.get("timeStamp", 0))
        )

        # The 'data' field contains the new implementation address
        data = log.get("data", "")
        new_impl = None
        if data and len(data) >= 66:
            addr = "0x" + data[-40:]
            if is_valid_evm_address(addr):
                new_impl = addr

        # Topic 1 (indexed param) might have previous implementation
        topics = log.get("topics", [])
        prev_impl = None
        if len(topics) > 1:
            addr = "0x" + topics[1][-40:]
            if is_valid_evm_address(addr):
                prev_impl = addr

        return UpgradeEvent(
            block_number=block_num,
            transaction_hash=tx_hash,
            timestamp=timestamp,
            previous_implementation=prev_impl,
            new_implementation=new_impl,
            triggered_by=None,  # Would need tx receipt for 'from'
        )
    except Exception as e:
        logger.debug("Failed to parse upgrade log: %s", e)
        return None


# ── Timelock Detection ──────────────────────────────────────────


async def check_timelock(address: str, chain: str) -> str | None:
    """Check if the proxy or its admin is behind a timelock.

    Returns 'active', 'bypassed', 'no_timelock', or None if unknown.
    """
    # Check if admin address is a known timelock contract
    info = await detect_proxy(address, chain)
    if not info.admin_address:
        return None

    admin = info.admin_address.lower()

    # Common timelock contract signatures

    # Check if admin is a known timelock address
    source_info = await _fetch_source_code(admin, chain)
    if source_info:
        contract_name = (source_info.get("ContractName", "") or "").lower()
        if "timelock" in contract_name:
            return "active"

    # Try to detect timelock by checking admin's own storage
    # Active timelocks have a minimum delay > 0
    try:

        # Use storage slot to check if admin has timelock delay
        delay_slot = "0x0000000000000000000000000000000000000000000000000000000000000002"
        result = await read_storage_slot(admin, delay_slot, chain)
        if result and int(result, 16) > 0:
            return "active"
    except Exception:
        pass

    return "no_timelock"


# ── Risk Assessment ──────────────────────────────────────────────


def _assess_risk(
    proxy_info: ProxyInfo,
    upgrades: list[UpgradeEvent],
    current_time: int,
) -> tuple[float, list[str]]:
    """Assess risk based on proxy type, upgrade frequency, and protections."""
    risk = 0.0
    factors = []

    # 1. No timelock is a risk
    risk += 10.0
    factors.append("No timelock detected - upgrades can be instant")

    # 2. Beacon proxies have higher attack surface
    if proxy_info.proxy_type == "beacon":
        risk += 15.0
        factors.append("Beacon proxy - multiple implementations share same beacon (wider attack surface)")

    # 3. UUPS proxies - upgrade logic in implementation (higher risk if buggy)
    if proxy_info.proxy_type == "eip1822":
        risk += 10.0
        factors.append(
            "UUPS proxy - upgrade logic in implementation contract (vulnerable if implementation has upgrade bug)"
        )

    # 4. Recent upgrades increase risk
    recent_count = sum(1 for u in upgrades if u.timestamp and current_time - u.timestamp < SUSPICIOUS_UPGRADE_WINDOW)
    if recent_count > 0:
        risk += min(recent_count * 15.0, 40.0)
        factors.append(f"{recent_count} upgrade(s) in the last 24 hours")

    # 5. Multiple upgrades in 30 days
    thirty_days = 30 * 86400
    month_count = sum(1 for u in upgrades if u.timestamp and current_time - u.timestamp < thirty_days)
    if month_count > 5:
        risk += 20.0
        factors.append(f"Frequent upgrades: {month_count} upgrades in 30 days")
    elif month_count > 2:
        risk += 10.0
        factors.append(f"Moderate upgrade frequency: {month_count} upgrades in 30 days")

    # 6. Admin address not set (unusual for proxies)
    if not proxy_info.admin_address and proxy_info.proxy_type:
        risk += 5.0
        factors.append("No admin address detected - upgrade authority unknown")

    # 7. Implementation address hasn't changed (low risk)
    upgrade_count = len(upgrades)
    if upgrade_count == 0 and proxy_info.is_proxy:
        risk = max(risk - 10.0, 5.0)
        factors.append("No upgrade history - proxy is stable")
    elif upgrade_count == 0:
        factors.append("Not a proxy - no upgrade risk")

    # Clamp
    risk = max(0.0, min(100.0, risk))
    return round(risk, 1), factors


# ── Main Analyzer ────────────────────────────────────────────────


class ContractUpgradeAnalyzer:
    """Main analyzer for contract upgrade monitoring."""

    async def analyze(self, contract_address: str, chain: str = "ethereum") -> ContractUpgradeReport:
        """Run complete upgrade monitoring analysis on a contract."""
        report = ContractUpgradeReport(
            contract_address=contract_address,
            chain=chain,
        )

        if not is_valid_evm_address(contract_address):
            raise ValueError(f"Invalid EVM address: {contract_address}")

        current_time = int(time.time())

        # Step 1: Detect proxy
        proxy_info = await detect_proxy(contract_address, chain)
        report.proxy_info = proxy_info

        if proxy_info.is_proxy:
            # Step 2: Check timelock
            timelock_status = await check_timelock(contract_address, chain)
            report.timelock_status = timelock_status

            # Step 3: Fetch upgrade history
            upgrades = await fetch_upgrade_history(contract_address, chain)
            report.upgrade_history = upgrades
            report.upgrade_count_30d = sum(
                1 for u in upgrades if u.timestamp and current_time - u.timestamp < 30 * 86400
            )

            # Step 4: Identify recent suspicious upgrades
            report.recent_suspicious_upgrades = [
                u for u in upgrades if u.timestamp and current_time - u.timestamp < SUSPICIOUS_UPGRADE_WINDOW
            ]

            # Step 5: Implementation age
            if upgrades:
                last_upgrade = max(upgrades, key=lambda u: u.timestamp or 0)
                report.last_upgrade_time = last_upgrade.timestamp
                if last_upgrade.timestamp:
                    report.implementation_age_days = (current_time - last_upgrade.timestamp) // 86400

            # Step 6: Admin privileges
            if proxy_info.detected_selectors:
                report.admin_privileges = [DANGEROUS_SELECTORS.get(s, s) for s in proxy_info.detected_selectors]

            # Step 7: Risk assessment
            risk, factors = _assess_risk(proxy_info, upgrades, current_time)
            report.risk_score = risk
            report.risk_factors = factors

            # Generate summary
            summary_parts = [f"Contract **{contract_address[:10]}...** on **{chain.title()}**"]
            if proxy_info.proxy_type_name:
                summary_parts.append(f"is a **{proxy_info.proxy_type_name}**")
            if proxy_info.implementation_address:
                summary_parts.append(f"→ implementation at **{proxy_info.implementation_address[:10]}...**")
            if report.upgrade_count_30d > 0:
                summary_parts.append(f"| **{report.upgrade_count_30d}** upgrades in 30 days")
            summary_parts.append(f"| Risk: **{report.risk_score}/100**")
            report.summary = " ".join(summary_parts)
        else:
            report.summary = (
                f"Contract **{contract_address[:10]}...** on **{chain.title()}** "
                f"does not appear to be a proxy contract. "
                f"Risk: **0/100** (no upgrade risk)"
            )
            report.risk_score = 0.0

        return report


# ── Formatter ────────────────────────────────────────────────────


def format_upgrade_report(report: ContractUpgradeReport) -> str:
    """Format the upgrade monitoring report for human-readable output."""
    lines = []
    lines.append("=" * 60)
    lines.append("CONTRACT UPGRADE MONITOR REPORT")
    lines.append("=" * 60)
    lines.append(f"Contract: {report.contract_address}")
    lines.append(f"Chain:    {report.chain.title()}")
    lines.append("")

    pi = report.proxy_info
    if pi and pi.is_proxy:
        lines.append(f"🔍 Proxy Detected: {pi.proxy_type_name or pi.proxy_type or 'Unknown'}")
        if pi.implementation_address:
            lines.append(f"   Implementation: {pi.implementation_address}")
        if pi.admin_address:
            lines.append(f"   Admin:          {pi.admin_address}")
        if pi.beacon_address:
            lines.append(f"   Beacon:         {pi.beacon_address}")
        lines.append(f"   Confidence:     {pi.confidence:.0%}")
        lines.append("")

        lines.append(f"⏱  Timelock: {report.timelock_status or 'Unknown'}")
        lines.append("")

        if report.upgrade_history:
            lines.append(f"📜 Upgrade History ({len(report.upgrade_history)} total):")
            for u in report.upgrade_history[:10]:  # Show last 10
                ts_str = ""
                if u.timestamp:
                    from datetime import datetime

                    ts_str = datetime.utcfromtimestamp(u.timestamp).strftime("%Y-%m-%d %H:%M UTC")
                lines.append(f"   TX: {u.transaction_hash[:20]}... | {ts_str}")
                if u.new_implementation:
                    lines.append(f"       New impl: {u.new_implementation[:20]}...")
            if len(report.upgrade_history) > 10:
                lines.append(f"   ... and {len(report.upgrade_history) - 10} more")
            lines.append("")
        else:
            lines.append("📜 No upgrade events found in explorer logs")
            lines.append("")

        lines.append(f"📈 Upgrade Count (30d): {report.upgrade_count_30d}")
        if report.implementation_age_days is not None:
            lines.append(f"📅 Current Impl Age: {report.implementation_age_days} days")
        lines.append("")

        if report.admin_privileges:
            lines.append("🔐 Admin Privileges Detected:")
            for priv in report.admin_privileges[:5]:
                lines.append(f"   • {priv}")
            lines.append("")

        lines.append("⚠️  RISK ASSESSMENT")
        lines.append(f"   Score: {report.risk_score}/100")
        for factor in report.risk_factors:
            lines.append(f"   • {factor}")
    else:
        lines.append("✅ Not a proxy contract - no upgrade risk detected.")
        if pi and pi.detected_selectors:
            lines.append("   (Proxy-like selectors found but no storage slot match)")

    lines.append("")
    lines.append("=" * 60)
    return "\n".join(lines)


# ── Singleton factory ────────────────────────────────────────────

_analyzer_instance: ContractUpgradeAnalyzer | None = None


def get_upgrade_analyzer() -> ContractUpgradeAnalyzer:
    """Get or create a singleton ContractUpgradeAnalyzer instance."""
    global _analyzer_instance
    if _analyzer_instance is None:
        _analyzer_instance = ContractUpgradeAnalyzer()
    return _analyzer_instance
