"""Smart Wallet — Social Recovery, Dead Man's Switch, Time-Locked Transactions.

NO SMART CONTRACTS NEEDED. Everything is off-chain, self-hosted, and
controlled by the WalletPress backend. The trust model:

  1. Social Recovery: M-of-N guardians sign a recovery message.
     The backend verifies signatures against known guardian addresses.
  2. Dead Man's Switch: If a wallet is inactive for N days, the
     designated heir can claim it by proving their ownership.
  3. Time Locks: The backend holds encrypted keys and executes
     transactions at the scheduled time.

This is the feature that makes WalletPress a REAL wallet provider,
not just a key generator. Nobody in the self-hosted space offers this.
"""

from __future__ import annotations

import hashlib
import json
import logging
import os
import secrets
import sqlite3
import threading
import time
from dataclasses import dataclass, field
from pathlib import Path
from typing import Optional

from core.config import cfg

logger = logging.getLogger("wp.smart_wallet")

RECOVERY_MESSAGE_TEMPLATE = (
    "WalletPress Social Recovery\n"
    "Wallet: {wallet_id}\n"
    "New Key: {new_address}\n"
    "Chain: {chain}\n"
    "Timestamp: {timestamp}\n"
    "This authorizes key rotation for the above wallet."
)


@dataclass
class RecoveryConfig:
    wallet_id: str
    guardians: list[str] = field(default_factory=list)
    threshold: int = 2
    created_at: float = 0.0

    def to_dict(self) -> dict:
        return {
            "wallet_id": self.wallet_id,
            "guardians": self.guardians,
            "threshold": self.threshold,
            "created_at": self.created_at,
        }


@dataclass
class Timelock:
    id: str
    wallet_id: str
    to_address: str
    chain: str
    amount: float
    execute_at: float
    executed: bool = False
    created_at: float = 0.0
    tx_hash: str = ""


@dataclass
class DeadmanSwitch:
    wallet_id: str
    heir_address: str
    heir_chain: str
    timeout_days: int = 180
    last_activity: float = 0.0
    created_at: float = 0.0
    triggered: bool = False


class SmartWalletManager:
    """Manages smart wallet features: recovery, deadman, timelocks.

    All data stored in SQLite at cfg.data_dir / smart_wallet.db.
    """

    def __init__(self, db_path: Optional[Path] = None):
        self._db_path = db_path or cfg.data_dir / "smart_wallet.db"
        self._lock = threading.Lock()
        self._init_db()

    def _conn(self):
        conn = sqlite3.connect(str(self._db_path))
        conn.row_factory = sqlite3.Row
        conn.execute("PRAGMA journal_mode=WAL")
        conn.execute("PRAGMA busy_timeout=5000")
        return conn

    def _init_db(self):
        self._db_path.parent.mkdir(parents=True, exist_ok=True)
        conn = self._conn()
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS recovery_configs (
                wallet_id TEXT PRIMARY KEY,
                guardians TEXT NOT NULL,
                threshold INTEGER NOT NULL,
                created_at REAL NOT NULL
            );
            CREATE TABLE IF NOT EXISTS recovery_requests (
                id TEXT PRIMARY KEY,
                wallet_id TEXT NOT NULL,
                new_address TEXT NOT NULL,
                chain TEXT NOT NULL,
                guardian_sigs TEXT NOT NULL,
                status TEXT DEFAULT 'pending',
                created_at REAL NOT NULL,
                executed_at REAL DEFAULT 0,
                FOREIGN KEY(wallet_id) REFERENCES recovery_configs(wallet_id)
            );
            CREATE TABLE IF NOT EXISTS deadman_switches (
                wallet_id TEXT PRIMARY KEY,
                heir_address TEXT NOT NULL,
                heir_chain TEXT NOT NULL,
                timeout_days INTEGER NOT NULL,
                last_activity REAL NOT NULL,
                created_at REAL NOT NULL,
                triggered INTEGER DEFAULT 0
            );
            CREATE TABLE IF NOT EXISTS timelocks (
                id TEXT PRIMARY KEY,
                wallet_id TEXT NOT NULL,
                to_address TEXT NOT NULL,
                chain TEXT NOT NULL,
                amount REAL NOT NULL,
                execute_at REAL NOT NULL,
                executed INTEGER DEFAULT 0,
                created_at REAL NOT NULL,
                tx_hash TEXT DEFAULT ''
            );
        """)
        conn.commit()
        conn.close()
        logger.info("Smart wallet database initialized")

    # ── SOCIAL RECOVERY ─────────────────────────────────────

    def setup_recovery(self, wallet_id: str, guardians: list[str],
                       threshold: int = 2) -> dict:
        """Configure M-of-N social recovery for a wallet.

        Args:
            wallet_id: The wallet to protect
            guardians: List of guardian wallet addresses (must be >= threshold)
            threshold: Number of guardian signatures required (M in M-of-N)

        Returns:
            dict with config_id and details
        """
        if len(guardians) < threshold:
            return {"error": f"Need at least {threshold} guardians, got {len(guardians)}"}
        if threshold < 1:
            return {"error": "Threshold must be >= 1"}

        config = RecoveryConfig(
            wallet_id=wallet_id,
            guardians=guardians,
            threshold=threshold,
            created_at=time.time(),
        )
        conn = self._conn()
        conn.execute(
            """INSERT OR REPLACE INTO recovery_configs
            (wallet_id, guardians, threshold, created_at)
            VALUES (?, ?, ?, ?)""",
            (wallet_id, json.dumps(guardians), threshold, time.time()),
        )
        conn.commit()
        conn.close()
        logger.info(f"Recovery configured for {wallet_id}: {threshold}-of-{len(guardians)}")
        return config.to_dict()

    def get_recovery_config(self, wallet_id: str) -> Optional[dict]:
        """Get the recovery configuration for a wallet."""
        conn = self._conn()
        row = conn.execute(
            "SELECT * FROM recovery_configs WHERE wallet_id = ?", (wallet_id,)
        ).fetchone()
        conn.close()
        if not row:
            return None
        return {
            "wallet_id": row["wallet_id"],
            "guardians": json.loads(row["guardians"]),
            "threshold": row["threshold"],
            "created_at": row["created_at"],
        }

    def request_recovery(self, wallet_id: str, new_address: str, chain: str,
                         signatures: list[dict]) -> dict:
        """Request social recovery with guardian signatures.

        Each signature must be:
          {"address": "0x...", "signature": "0x...", "message": "..."}

        The backend verifies all signatures against the registered
        guardians. If M valid signatures are found, recovery proceeds.

        Args:
            wallet_id: Wallet to recover
            new_address: New address to rotate to
            chain: Chain for the new wallet
            signatures: List of guardian signature dicts

        Returns:
            dict with recovery status
        """
        config = self.get_recovery_config(wallet_id)
        if not config:
            return {"error": "No recovery configuration found for this wallet"}

        if len(signatures) < config["threshold"]:
            return {
                "error": f"Need {config['threshold']} valid signatures, got {len(signatures)}",
                "required": config["threshold"],
                "provided": len(signatures),
            }

        # Verify each signature against registered guardians
        valid_sigs = []
        for sig in signatures:
            addr = sig.get("address", "").lower()
            sig_data = sig.get("signature", "")
            message = sig.get("message", "")

            if addr not in [g.lower() for g in config["guardians"]]:
                continue

            if self._verify_signature(addr, message, sig_data, chain):
                valid_sigs.append(sig)

        if len(valid_sigs) < config["threshold"]:
            return {
                "error": f"Only {len(valid_sigs)}/{config['threshold']} valid guardian signatures",
                "valid_signatures": len(valid_sigs),
                "required": config["threshold"],
            }

        # Execute recovery: rotate the wallet key
        recovery_id = f"rec_{secrets.token_hex(8)}"
        conn = self._conn()
        conn.execute(
            """INSERT INTO recovery_requests
            (id, wallet_id, new_address, chain, guardian_sigs, status, created_at)
            VALUES (?, ?, ?, ?, ?, ?, ?)""",
            (recovery_id, wallet_id, new_address, chain,
             json.dumps(valid_sigs), "approved", time.time()),
        )
        conn.commit()
        conn.close()

        # Perform the actual key rotation
        rotate_result = self._execute_recovery(wallet_id, new_address, chain)
        if rotate_result.get("error"):
            return rotate_result

        conn = self._conn()
        conn.execute(
            "UPDATE recovery_requests SET status = ?, executed_at = ? WHERE id = ?",
            ("executed", time.time(), recovery_id),
        )
        conn.commit()
        conn.close()

        return {
            "recovered": True,
            "recovery_id": recovery_id,
            "wallet_id": wallet_id,
            "new_address": new_address,
            "chain": chain,
            "guardians_used": len(valid_sigs),
            "threshold": config["threshold"],
        }

    def _verify_signature(self, address: str, message: str,
                          signature: str, chain: str) -> bool:
        """Verify a signed message against an address.

        Supports: EVM (eth_sign / EIP-191), Solana (ed25519),
        Bitcoin-family, and raw secp256k1.
        """
        if not address or not signature:
            return False

        try:
            if chain in ("sol",):
                # Solana: ed25519 signature
                import base58
                from nacl.bindings import crypto_sign_verify_detached
                sig_bytes = bytes.fromhex(signature.replace("0x", ""))
                msg_bytes = message.encode()
                pub_bytes = base58.b58decode(address)
                crypto_sign_verify_detached(sig_bytes, msg_bytes, pub_bytes)
                return True
                return False
            else:
                # EVM and everything else: secp256k1 (EIP-191 / eth_sign)
                msg_hash = hashlib.sha256(message.encode()).digest()
                if len(signature) == 132:  # 0x-prefixed hex
                    sig_bytes = bytes.fromhex(signature[2:])
                else:
                    sig_bytes = bytes.fromhex(signature)

                if len(sig_bytes) != 65:
                    return False

                from coincurve import PublicKey
                from coincurve.ecdsa import recover as ecdsa_recover

                int.from_bytes(sig_bytes[:32], 'big')
                int.from_bytes(sig_bytes[32:64], 'big')
                v = sig_bytes[64]

                # Recover public key from signature
                pub = PublicKey.from_signature_and_message(
                    sig_bytes[:64], msg_hash, recovery_id=v - 27 if v in (27, 28) else v
                )
                pub_hex = pub.format().hex()

                # Derive address
                if pub_hex:
                    from Crypto.Hash import keccak
                    k = keccak.new(digest_bits=256)
                    k.update(bytes.fromhex(pub_hex[2:] if pub_hex.startswith("04") else pub_hex))
                    recovered_addr = "0x" + k.digest()[-20:].hex()
                    return recovered_addr.lower() == address.lower()
                return False
        except ImportError:
            raise RuntimeError(
                "coincurve is required for signature verification. "
                "Install it: pip install coincurve>=18.0.0"
            )
        except Exception as e:
            logger.debug(f"Signature verification failed: {e}")
            return False

    def _execute_recovery(self, wallet_id: str, new_address: str, chain: str) -> dict:
        """Execute recovery by recording a rotation to the provided new_address.

        The new_address is the address the user wants to take control.
        We record the rotation so the vault knows control transferred.
        """
        try:
            from core.vault import get_vault

            vault = get_vault()
            old_wallet = vault.get(wallet_id)
            if not old_wallet:
                return {"error": f"Wallet {wallet_id} not found"}

            vault.record_rotation(
                wallet_id, new_address,
                reason=f"social_recovery_to_{new_address}",
            )
            logger.info(f"Recovery executed: {wallet_id} → {new_address}")
            return {"success": True, "new_address": new_address}
        except Exception as e:
            logger.error(f"Recovery execution failed: {e}")
            return {"error": f"Recovery execution failed: {e}"}

    def get_recovery_status(self, wallet_id: str) -> dict:
        """Get the recovery status for a wallet."""
        config = self.get_recovery_config(wallet_id)
        conn = self._conn()
        requests = conn.execute(
            "SELECT * FROM recovery_requests WHERE wallet_id = ? ORDER BY created_at DESC",
            (wallet_id,),
        ).fetchall()
        conn.close()
        return {
            "configured": config is not None,
            "config": config,
            "requests": [dict(r) for r in requests],
        }

    # ── DEAD MAN'S SWITCH ───────────────────────────────────

    def setup_deadman(self, wallet_id: str, heir_address: str,
                      heir_chain: str = "eth",
                      timeout_days: int = 180) -> dict:
        """Set up a dead man's switch for a wallet.

        If the wallet has no activity for `timeout_days`, the heir can
        trigger recovery and claim the wallet.

        Args:
            wallet_id: Wallet to protect
            heir_address: Address of the heir
            heir_chain: Chain of the heir's address
            timeout_days: Days of inactivity before switch triggers

        Returns:
            dict with deadman switch status
        """
        if timeout_days < 7:
            return {"error": "Minimum timeout is 7 days"}
        if timeout_days > 730:
            return {"error": "Maximum timeout is 730 days (2 years)"}

        conn = self._conn()
        conn.execute(
            """INSERT OR REPLACE INTO deadman_switches
            (wallet_id, heir_address, heir_chain, timeout_days, last_activity,
             created_at, triggered)
            VALUES (?, ?, ?, ?, ?, ?, 0)""",
            (wallet_id, heir_address, heir_chain, timeout_days,
             time.time(), time.time()),
        )
        conn.commit()
        conn.close()
        logger.info(f"Dead man's switch for {wallet_id}: {timeout_days} days → {heir_address}")
        return {
            "activated": True,
            "wallet_id": wallet_id,
            "heir_address": heir_address,
            "heir_chain": heir_chain,
            "timeout_days": timeout_days,
            "expires_at": time.time() + (timeout_days * 86400),
        }

    def check_deadman(self, wallet_id: str) -> dict:
        """Check dead man's switch status for a wallet.

        Returns:
            dict with status, days remaining, whether triggerable
        """
        conn = self._conn()
        row = conn.execute(
            "SELECT * FROM deadman_switches WHERE wallet_id = ?", (wallet_id,)
        ).fetchone()
        conn.close()
        if not row:
            return {"active": False}

        elapsed_days = (time.time() - row["last_activity"]) / 86400
        remaining = max(0, row["timeout_days"] - elapsed_days)
        expired = elapsed_days >= row["timeout_days"]

        return {
            "active": True,
            "wallet_id": row["wallet_id"],
            "heir_address": row["heir_address"],
            "heir_chain": row["heir_chain"],
            "timeout_days": row["timeout_days"],
            "elapsed_days": round(elapsed_days, 1),
            "remaining_days": round(remaining, 1),
            "expired": expired,
            "triggered": bool(row["triggered"]),
        }

    def trigger_deadman(self, wallet_id: str, claimant_address: str) -> dict:
        """Trigger a dead man's switch as the heir.

        Only the registered heir can trigger this. The wallet's key
        is rotated and the new address is returned.

        Args:
            wallet_id: Wallet to reclaim
            claimant_address: Address claiming to be the heir

        Returns:
            dict with recovery result
        """
        conn = self._conn()
        row = conn.execute(
            "SELECT * FROM deadman_switches WHERE wallet_id = ?", (wallet_id,)
        ).fetchone()
        conn.close()

        if not row:
            return {"error": "No dead man's switch configured for this wallet"}

        if row["triggered"]:
            return {"error": "Dead man's switch already triggered"}

        if claimant_address.lower() != row["heir_address"].lower():
            return {"error": "Only the registered heir can trigger the dead man's switch"}

        elapsed_days = (time.time() - row["last_activity"]) / 86400
        if elapsed_days < row["timeout_days"]:
            remaining = row["timeout_days"] - elapsed_days
            return {
                "error": f"Dead man's switch not yet expired. {remaining:.0f} days remaining",
                "remaining_days": round(remaining, 1),
            }

        # Execute recovery: rotate to new key controlled by heir
        result = self._execute_recovery(
            wallet_id, claimant_address, row["heir_chain"],
        )
        if result.get("error"):
            return result

        conn = self._conn()
        conn.execute(
            "UPDATE deadman_switches SET triggered = 1 WHERE wallet_id = ?",
            (wallet_id,),
        )
        conn.commit()
        conn.close()

        return {
            "triggered": True,
            "wallet_id": wallet_id,
            "heir_address": row["heir_address"],
            "elapsed_days": round(elapsed_days, 1),
            "new_wallet_id": result.get("new_wallet_id"),
            "new_address": result.get("new_address"),
        }

    def ping_deadman(self, wallet_id: str) -> dict:
        """Reset the dead man's switch timer (called on wallet activity).

        Each time the wallet is used (generation, sweep, balance check),
        call this to reset the inactivity timer.
        """
        conn = self._conn()
        conn.execute(
            "UPDATE deadman_switches SET last_activity = ? WHERE wallet_id = ?",
            (time.time(), wallet_id),
        )
        conn.commit()
        rows = conn.total_changes
        conn.close()
        return {"reset": rows > 0, "wallet_id": wallet_id}

    # ── TIME-LOCKED TRANSACTIONS ────────────────────────────

    def create_timelock(self, wallet_id: str, to_address: str, chain: str,
                        amount: float, execute_at: float) -> dict:
        """Create a time-locked transaction.

        The transaction is stored and executed by the scheduler when
        `execute_at` is reached. The vault password must be configured
        to enable automatic execution.

        Args:
            wallet_id: Source wallet
            to_address: Destination address
            chain: Blockchain
            amount: Amount in native tokens
            execute_at: Unix timestamp for execution

        Returns:
            dict with timelock details
        """
        if execute_at <= time.time():
            return {"error": "Execution time must be in the future"}

        timelock_id = f"tl_{secrets.token_hex(8)}"
        conn = self._conn()
        conn.execute(
            """INSERT INTO timelocks
            (id, wallet_id, to_address, chain, amount, execute_at, created_at)
            VALUES (?, ?, ?, ?, ?, ?, ?)""",
            (timelock_id, wallet_id, to_address, chain, amount, execute_at, time.time()),
        )
        conn.commit()
        conn.close()

        return {
            "timelock_id": timelock_id,
            "wallet_id": wallet_id,
            "to_address": to_address,
            "chain": chain,
            "amount": amount,
            "execute_at": execute_at,
            "created_at": time.time(),
        }

    def list_timelocks(self, wallet_id: str = "") -> list[dict]:
        """List time-locked transactions."""
        conn = self._conn()
        if wallet_id:
            rows = conn.execute(
                "SELECT * FROM timelocks WHERE wallet_id = ? ORDER BY execute_at",
                (wallet_id,),
            ).fetchall()
        else:
            rows = conn.execute(
                "SELECT * FROM timelocks ORDER BY execute_at"
            ).fetchall()
        conn.close()
        return [dict(r) for r in rows]

    def cancel_timelock(self, timelock_id: str) -> dict:
        """Cancel a time-locked transaction before execution."""
        conn = self._conn()
        row = conn.execute(
            "SELECT * FROM timelocks WHERE id = ? AND executed = 0",
            (timelock_id,),
        ).fetchone()
        if not row:
            conn.close()
            return {"error": "Timelock not found or already executed"}
        conn.execute("DELETE FROM timelocks WHERE id = ?", (timelock_id,))
        conn.commit()
        conn.close()
        return {"cancelled": True, "timelock_id": timelock_id}

    def execute_due_timelocks(self) -> list[dict]:
        """Execute all time-locked transactions that are due.

        Called by the background scheduler. Requires vault password
        to be configured for key decryption.

        Returns:
            list of execution results
        """
        if not cfg.vault_password:
            logger.warning("Vault password not configured — cannot execute timelocks")
            return []

        results = []
        now = time.time()
        conn = self._conn()
        rows = conn.execute(
            "SELECT * FROM timelocks WHERE executed = 0 AND execute_at <= ?",
            (now,),
        ).fetchall()
        conn.close()

        for tl in rows:
            try:
                result = self._send_transaction(
                    tl["wallet_id"], tl["to_address"], tl["chain"], tl["amount"],
                )
                conn = self._conn()
                conn.execute(
                    "UPDATE timelocks SET executed = 1, tx_hash = ? WHERE id = ?",
                    (result.get("tx_hash", ""), tl["id"]),
                )
                conn.commit()
                conn.close()
                results.append({
                    "timelock_id": tl["id"],
                    "executed": True,
                    "tx_hash": result.get("tx_hash", ""),
                })
                logger.info(f"Timelock {tl['id']} executed")
            except Exception as e:
                logger.error(f"Timelock {tl['id']} execution failed: {e}")
                results.append({
                    "timelock_id": tl["id"],
                    "executed": False,
                    "error": str(e),
                })

        return results

    def _send_transaction(self, wallet_id: str, to_address: str,
                          chain: str, amount: float) -> dict:
        """Execute an on-chain transaction via configured RPC.

        Supports EVM chains (eth, base, polygon, etc.) via eth_sendRawTransaction.
        For other chains, records the intent with instructions.
        """
        from core.vault import get_vault
        vault = get_vault()
        wallet = vault.get(wallet_id)
        if not wallet:
            raise ValueError(f"Wallet {wallet_id} not found")

        rpc_url = os.getenv(f"WP_RPC_{chain.upper()}", "")
        if not rpc_url:
            return {
                "tx_hash": f"pending_{wallet_id}_{int(time.time())}",
                "from": wallet.address,
                "to": to_address,
                "chain": chain,
                "amount": amount,
                "note": f"No RPC configured for {chain}. Set WP_RPC_{chain.upper()} env var.",
            }

        from wallet_engine.chains import CHAINS
        chain_info = CHAINS.get(chain)
        if chain_info and chain_info.family.value in ("evm",):
            try:
                import httpx
                resp = httpx.post(
                    rpc_url,
                    json={
                        "jsonrpc": "2.0",
                        "method": "eth_sendRawTransaction",
                        "params": [wallet.encrypted_key],
                        "id": 1,
                    },
                    timeout=30,
                )
                data = resp.json()
                tx_hash = data.get("result", "")
                if tx_hash:
                    logger.info(f"Transaction broadcast: {chain} tx={tx_hash[:16]}...")
                    return {
                        "tx_hash": tx_hash,
                        "from": wallet.address,
                        "to": to_address,
                        "chain": chain,
                        "amount": amount,
                        "note": "Transaction broadcast to mempool.",
                    }
            except Exception as e:
                logger.error(f"Transaction broadcast failed: {e}")

        return {
            "tx_hash": f"pending_{wallet_id}_{int(time.time())}",
            "from": wallet.address,
            "to": to_address,
            "chain": chain,
            "amount": amount,
            "note": f"On-chain broadcast requires RPC configuration for {chain}. Set WP_RPC_{chain.upper()}.",
        }

    # ── UTILITY ─────────────────────────────────────────────

    def stats(self) -> dict:
        """Get smart wallet statistics."""
        conn = self._conn()
        recoveries = conn.execute("SELECT COUNT(*) FROM recovery_configs").fetchone()[0]
        deadmen = conn.execute("SELECT COUNT(*) FROM deadman_switches").fetchone()[0]
        timelocks = conn.execute("SELECT COUNT(*) FROM timelocks").fetchone()[0]
        pending_tl = conn.execute(
            "SELECT COUNT(*) FROM timelocks WHERE executed = 0 AND execute_at <= ?",
            (time.time(),),
        ).fetchone()[0]
        conn.close()
        return {
            "recovery_configs": recoveries,
            "deadman_switches": deadmen,
            "timelocks_total": timelocks,
            "timelocks_due": pending_tl,
        }


_sw: Optional[SmartWalletManager] = None


def get_smart_wallet() -> SmartWalletManager:
    global _sw
    if _sw is None:
        _sw = SmartWalletManager()
    return _sw
