"""Automatic blockchain payment detector.

Polls multiple public APIs every ~25s for incoming transactions to our
configured wallet addresses (TRC20, BEP20, BTC). When an inbound transfer
matches an active pending deposit's UNIQUE amount, the deposit is
auto-approved and the user's wallet is credited.

Resilient multi-provider fallbacks:
  * TRC20: TronGrid -> TronScan classic -> TronScan v2 filter API
  * BEP20: 5 public BSC RPCs via eth_getLogs (no API key required)
  * BTC:   mempool.space -> blockstream.info
"""
from __future__ import annotations
import asyncio, logging, time
from typing import Optional, List, Dict, Any
import aiohttp

from database.db import DB
from config import (USDT_TRC20_ADDRESS, USDT_BEP20_ADDRESS, BTC_ADDRESS,
                    BSCSCAN_API_KEY, TRONGRID_API_KEY)
from utils.helpers import money
from services.notifications import notify_staff
from services.rewards import mark_active_and_reward

log = logging.getLogger("cartcraft.watcher")

POLL_INTERVAL = 25
EXPIRY_TICK   = 30
USDT_TRC20_CONTRACT = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"
USDT_BEP20_CONTRACT = "0x55d398326f99059fF775485246999027B3197955"

USDT_DECIMALS = 6           # TRC20 USDT
USDT_BEP20_DECIMALS = 18   # BSC-USD (BEP20) uses 18 decimals
BTC_DECIMALS  = 8
USDT_TOLERANCE = 0.005      # ±0.5 cent
BTC_TOLERANCE  = 0.0        # exact match (unique sat slots)

# ERC20 Transfer(address,address,uint256) topic
ERC20_TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"

BSC_RPCS = [
    "https://bsc-dataseed.binance.org",
    "https://bsc-dataseed1.defibit.io",
    "https://bsc-dataseed1.ninicoin.io",
    "https://rpc.ankr.com/bsc",
    "https://bsc.publicnode.com",
]


# --------------------------------------------------------------------------
# DB helpers
# --------------------------------------------------------------------------

async def _active_deposits(method: str):
    return await DB.fetchall(
        "SELECT id, user_id, amount, unique_amount, address, expires_at, "
        "       chat_id, message_id, status "
        "FROM deposits "
        "WHERE method=? AND status IN ('pending_payment','payment_detected','confirming') "
        "  AND unique_amount IS NOT NULL",
        (method,))


async def _txid_seen(txid: str) -> bool:
    row = await DB.fetchone(
        "SELECT 1 FROM deposits WHERE detected_txid=?", (txid,))
    return bool(row)


async def _complete_deposit(bot, dep, txid: str, usd_amount: float):
    cur = await DB.execute(
        "UPDATE deposits SET status='payment_completed', detected_txid=?, "
        "       reviewed_at=datetime('now'), confirmations=1 "
        "WHERE id=? AND status IN ('pending_payment','payment_detected','confirming')",
        (txid, dep["id"]))
    if cur.rowcount == 0:
        return
    await DB.execute(
        "UPDATE users SET balance = balance + ?, total_deposits = total_deposits + ? "
        "WHERE user_id=?", (usd_amount, usd_amount, dep["user_id"]))
    try:
        await bot.send_message(
            dep["user_id"],
            f"✅ <b>Payment Completed</b>\n\n"
            f"Deposit #{dep['id']} of <b>{money(usd_amount)}</b> was detected "
            f"on-chain and credited to your wallet.\n\n"
            f"TX: <code>{txid[:24]}…</code>",
            parse_mode="HTML")
    except Exception:
        pass
    if dep["chat_id"] and dep["message_id"]:
        try:
            await bot.edit_message_text(
                chat_id=dep["chat_id"], message_id=dep["message_id"],
                text=(f"✅ <b>Payment Completed</b>\n\n"
                      f"Deposit #{dep['id']} — <b>{money(usd_amount)}</b>\n"
                      f"TX: <code>{txid[:32]}…</code>\n\n"
                      f"Your wallet has been credited automatically."),
                parse_mode="HTML")
        except Exception:
            pass
    try:
        await notify_staff(bot,
            f"🟢 <b>Auto-Approved Deposit #{dep['id']}</b>\n"
            f"User: <code>{dep['user_id']}</code>\n"
            f"Amount: {money(usd_amount)}\n"
            f"TX: <code>{txid}</code>")
    except Exception:
        pass
    try:
        await mark_active_and_reward(bot, dep["user_id"])
    except Exception:
        pass


async def _mark_detected(bot, dep, txid: str):
    cur = await DB.execute(
        "UPDATE deposits SET status='payment_detected', detected_txid=? "
        "WHERE id=? AND status='pending_payment'", (txid, dep["id"]))
    if cur.rowcount == 0:
        return
    try:
        await bot.send_message(
            dep["user_id"],
            f"🔎 <b>Payment Detected</b>\n\nDeposit #{dep['id']} — waiting for "
            f"network confirmation…", parse_mode="HTML")
    except Exception:
        pass


# --------------------------------------------------------------------------
# TRC20 watcher (with 3-layer fallback)
# --------------------------------------------------------------------------

async def _trc20_trongrid(session) -> List[Dict[str, Any]]:
    url = (f"https://api.trongrid.io/v1/accounts/{USDT_TRC20_ADDRESS}"
           f"/transactions/trc20?limit=50&only_to=true"
           f"&contract_address={USDT_TRC20_CONTRACT}")
    headers = {"TRON-PRO-API-KEY": TRONGRID_API_KEY} if TRONGRID_API_KEY else {}
    async with session.get(url, headers=headers,
                           timeout=aiohttp.ClientTimeout(total=15)) as r:
        j = await r.json(content_type=None)
    out = []
    for tx in (j.get("data") or []):
        if (tx.get("to") or "") != USDT_TRC20_ADDRESS:
            continue
        try:
            raw = int(tx.get("value") or 0)
        except Exception:
            continue
        out.append({"txid": tx.get("transaction_id") or "",
                    "amount": raw / 10 ** USDT_DECIMALS})
    return out


async def _trc20_tronscan(session) -> List[Dict[str, Any]]:
    url = (f"https://apilist.tronscanapi.com/api/token_trc20/transfers"
           f"?limit=50&start=0&direction=2&relatedAddress={USDT_TRC20_ADDRESS}"
           f"&contract_address={USDT_TRC20_CONTRACT}")
    async with session.get(url, timeout=aiohttp.ClientTimeout(total=15)) as r:
        j = await r.json(content_type=None)
    out = []
    for tx in (j.get("token_transfers") or j.get("data") or []):
        if (tx.get("to_address") or "") != USDT_TRC20_ADDRESS:
            continue
        try:
            decimals = int((tx.get("tokenInfo") or {}).get("tokenDecimal") or USDT_DECIMALS)
            raw = int(tx.get("quant") or tx.get("amount_str") or 0)
        except Exception:
            continue
        out.append({"txid": tx.get("transaction_id") or tx.get("hash") or "",
                    "amount": raw / 10 ** decimals})
    return out


async def _trc20_tronscan_v2(session) -> List[Dict[str, Any]]:
    url = (f"https://apilist.tronscanapi.com/api/filter/trc20/transfers"
           f"?limit=50&start=0&relatedAddress={USDT_TRC20_ADDRESS}"
           f"&contract_address={USDT_TRC20_CONTRACT}")
    async with session.get(url, timeout=aiohttp.ClientTimeout(total=15)) as r:
        j = await r.json(content_type=None)
    out = []
    for tx in (j.get("token_transfers") or j.get("data") or []):
        if (tx.get("to_address") or "") != USDT_TRC20_ADDRESS:
            continue
        try:
            decimals = int((tx.get("tokenInfo") or {}).get("tokenDecimal") or USDT_DECIMALS)
            raw = int(tx.get("quant") or tx.get("amount_str") or 0)
        except Exception:
            continue
        out.append({"txid": tx.get("transaction_id") or tx.get("hash") or "",
                    "amount": raw / 10 ** decimals})
    return out


async def _poll_trc20(session: aiohttp.ClientSession, bot):
    if not USDT_TRC20_ADDRESS:
        return
    pending = await _active_deposits("trc20")
    if not pending:
        return
    txs = []
    for fn in (_trc20_trongrid, _trc20_tronscan, _trc20_tronscan_v2):
        try:
            txs = await fn(session)
            if txs:
                break
        except Exception as e:
            log.debug("trc20 source %s failed: %s", fn.__name__, e)
    for tx in txs:
        txid = tx["txid"]; usdt = tx["amount"]
        if not txid or await _txid_seen(txid):
            continue
        for d in pending:
            target = float(d["unique_amount"] or 0)
            if abs(usdt - target) <= USDT_TOLERANCE:
                log.info("TRC20 match dep=%s tx=%s usdt=%s target=%s",
                         d["id"], txid, usdt, target)
                await _complete_deposit(bot, d, txid, float(d["amount"]))
                break


# --------------------------------------------------------------------------
# BEP20 watcher — multi-RPC eth_getLogs (no API key needed)
# --------------------------------------------------------------------------

def _addr_topic(addr: str) -> str:
    return "0x" + "0" * 24 + addr.lower().replace("0x", "")


async def _rpc_call(session, url: str, method: str, params: list):
    async with session.post(url, json={
        "jsonrpc": "2.0", "id": 1, "method": method, "params": params
    }, timeout=aiohttp.ClientTimeout(total=12)) as r:
        return await r.json(content_type=None)


async def _bep20_via_rpc(session, rpc: str) -> List[Dict[str, Any]]:
    blk = await _rpc_call(session, rpc, "eth_blockNumber", [])
    latest = int(blk["result"], 16)
    # last ~600 blocks (~30 min, BSC ~3s blocks)
    from_block = hex(max(latest - 600, 0))
    to_topic = _addr_topic(USDT_BEP20_ADDRESS)
    logs = await _rpc_call(session, rpc, "eth_getLogs", [{
        "fromBlock": from_block,
        "toBlock": "latest",
        "address": USDT_BEP20_CONTRACT,
        "topics": [ERC20_TRANSFER_TOPIC, None, to_topic],
    }])
    out = []
    for ev in (logs.get("result") or []):
        try:
            raw = int(ev["data"], 16)
        except Exception:
            continue
        out.append({"txid": ev.get("transactionHash") or "",
                    "amount": raw / 10 ** USDT_BEP20_DECIMALS})
    return out


async def _poll_bep20(session: aiohttp.ClientSession, bot):
    if not USDT_BEP20_ADDRESS:
        return
    pending = await _active_deposits("bep20")
    if not pending:
        return
    txs: List[Dict[str, Any]] = []
    for rpc in BSC_RPCS:
        try:
            txs = await _bep20_via_rpc(session, rpc)
            if txs is not None:
                break
        except Exception as e:
            log.debug("bep20 rpc %s failed: %s", rpc, e)
    for tx in txs:
        txid = tx["txid"]; usdt = tx["amount"]
        if not txid or await _txid_seen(txid):
            continue
        for d in pending:
            target = float(d["unique_amount"] or 0)
            if abs(usdt - target) <= USDT_TOLERANCE:
                log.info("BEP20 match dep=%s tx=%s usdt=%s target=%s",
                         d["id"], txid, usdt, target)
                await _complete_deposit(bot, d, txid, float(d["amount"]))
                break


# --------------------------------------------------------------------------
# BTC watcher (mempool.space + blockstream fallback)
# --------------------------------------------------------------------------

async def _btc_fetch(session, base: str):
    url = f"{base}/address/{BTC_ADDRESS}/txs"
    async with session.get(url, timeout=aiohttp.ClientTimeout(total=15)) as r:
        return await r.json(content_type=None)


async def _poll_btc(session: aiohttp.ClientSession, bot):
    if not BTC_ADDRESS:
        return
    pending = await _active_deposits("btc")
    if not pending:
        return
    txs = None
    for base in ("https://mempool.space/api", "https://blockstream.info/api"):
        try:
            txs = await _btc_fetch(session, base)
            if isinstance(txs, list):
                break
        except Exception as e:
            log.debug("btc source %s failed: %s", base, e)
    if not isinstance(txs, list):
        return
    for tx in txs[:50]:
        txid = tx.get("txid") or ""
        if not txid:
            continue
        sats = 0
        for vout in (tx.get("vout") or []):
            if (vout.get("scriptpubkey_address") or "") == BTC_ADDRESS:
                sats += int(vout.get("value") or 0)
        if sats <= 0:
            continue
        btc_amount = sats / 10 ** BTC_DECIMALS
        confirmed = bool((tx.get("status") or {}).get("confirmed"))
        for d in pending:
            target_btc = float(d["unique_amount"] or 0)
            if abs(btc_amount - target_btc) <= BTC_TOLERANCE + 1e-12:
                if await _txid_seen(txid):
                    continue
                if not confirmed:
                    await _mark_detected(bot, d, txid)
                else:
                    await _complete_deposit(bot, d, txid, float(d["amount"]))
                break


# --------------------------------------------------------------------------
# Manual TX ID verification (called from handlers/deposits.py)
# --------------------------------------------------------------------------

async def verify_txid(method: str, txid: str, expected_amount: float) -> Optional[dict]:
    """Look up a transaction directly and check it matches `expected_amount`
    sent to our wallet. Returns {'amount':..., 'txid':...} on success, else None."""
    txid = txid.strip()
    if not txid:
        return None
    timeout = aiohttp.ClientTimeout(total=15)
    async with aiohttp.ClientSession() as session:
        try:
            if method == "trc20":
                if not USDT_TRC20_ADDRESS:
                    return None
                url = f"https://apilist.tronscanapi.com/api/transaction-info?hash={txid}"
                async with session.get(url, timeout=timeout) as r:
                    j = await r.json(content_type=None)
                transfers = j.get("trc20TransferInfo") or []
                for t in transfers:
                    if (t.get("to_address") or "") != USDT_TRC20_ADDRESS:
                        continue
                    if (t.get("contract_address") or "") != USDT_TRC20_CONTRACT:
                        continue
                    decimals = int(t.get("decimals") or USDT_DECIMALS)
                    raw = int(t.get("amount_str") or 0)
                    amt = raw / 10 ** decimals
                    if abs(amt - expected_amount) <= USDT_TOLERANCE:
                        return {"amount": amt, "txid": txid}
                return None

            if method == "bep20":
                if not USDT_BEP20_ADDRESS:
                    return None
                for rpc in BSC_RPCS:
                    try:
                        r = await _rpc_call(session, rpc,
                                            "eth_getTransactionReceipt", [txid])
                        rcpt = r.get("result")
                        if not rcpt:
                            continue
                        for ev in (rcpt.get("logs") or []):
                            if (ev.get("address") or "").lower() != USDT_BEP20_CONTRACT.lower():
                                continue
                            topics = ev.get("topics") or []
                            if len(topics) < 3 or topics[0].lower() != ERC20_TRANSFER_TOPIC:
                                continue
                            if topics[2].lower() != _addr_topic(USDT_BEP20_ADDRESS):
                                continue
                            raw = int(ev["data"], 16)
                            amt = raw / 10 ** USDT_BEP20_DECIMALS
                            if abs(amt - expected_amount) <= USDT_TOLERANCE:
                                return {"amount": amt, "txid": txid}
                        return None
                    except Exception:
                        continue
                return None

            if method == "btc":
                if not BTC_ADDRESS:
                    return None
                for base in ("https://mempool.space/api", "https://blockstream.info/api"):
                    try:
                        async with session.get(f"{base}/tx/{txid}", timeout=timeout) as r:
                            tx = await r.json(content_type=None)
                        sats = 0
                        for vout in (tx.get("vout") or []):
                            if (vout.get("scriptpubkey_address") or "") == BTC_ADDRESS:
                                sats += int(vout.get("value") or 0)
                        if sats <= 0:
                            continue
                        btc = sats / 10 ** BTC_DECIMALS
                        if abs(btc - expected_amount) <= BTC_TOLERANCE + 1e-12:
                            return {"amount": btc, "txid": txid}
                    except Exception:
                        continue
                return None
        except Exception as e:
            log.warning("verify_txid error: %s", e)
            return None
    return None


# --------------------------------------------------------------------------
# Expiry sweep + runner
# --------------------------------------------------------------------------

async def _sweep_expired(bot):
    rows = await DB.fetchall(
        "SELECT id, user_id, chat_id, message_id, amount FROM deposits "
        "WHERE status IN ('pending_payment','payment_detected') "
        "  AND expires_at IS NOT NULL "
        "  AND datetime(expires_at) <= datetime('now')")
    for d in rows:
        cur = await DB.execute(
            "UPDATE deposits SET status='expired' WHERE id=? "
            "AND status IN ('pending_payment','payment_detected')", (d["id"],))
        if cur.rowcount == 0:
            continue
        try:
            await bot.send_message(
                d["user_id"],
                f"⌛ <b>Deposit #{d['id']} expired.</b>\n\n"
                f"No matching payment of {money(d['amount'])} was detected in time. "
                f"Tap 💳 Deposit to start a new request.",
                parse_mode="HTML")
        except Exception:
            pass
        if d["chat_id"] and d["message_id"]:
            try:
                await bot.edit_message_text(
                    chat_id=d["chat_id"], message_id=d["message_id"],
                    text=f"⌛ <b>Deposit #{d['id']} expired</b> — please start a new one.",
                    parse_mode="HTML")
            except Exception:
                pass


_task: Optional[asyncio.Task] = None


async def _runner(bot):
    log.info("Chain watcher started (trc20=%s bep20=%s btc=%s)",
             bool(USDT_TRC20_ADDRESS), bool(USDT_BEP20_ADDRESS), bool(BTC_ADDRESS))
    last_expiry = 0.0
    async with aiohttp.ClientSession(headers={"User-Agent": "CartCraftBot/1.0"}) as session:
        while True:
            try:
                await asyncio.gather(
                    _poll_trc20(session, bot),
                    _poll_bep20(session, bot),
                    _poll_btc(session, bot),
                    return_exceptions=True,
                )
                if time.time() - last_expiry > EXPIRY_TICK:
                    await _sweep_expired(bot)
                    last_expiry = time.time()
            except asyncio.CancelledError:
                raise
            except Exception as e:
                log.warning("watcher loop error: %s", e)
            await asyncio.sleep(POLL_INTERVAL)


def start(bot) -> None:
    global _task
    if _task and not _task.done():
        return
    _task = asyncio.create_task(_runner(bot), name="chain_watcher")


async def stop() -> None:
    global _task
    if _task:
        _task.cancel()
        try:
            await _task
        except Exception:
            pass
        _task = None
