"""Automatic blockchain payment detector.

Polls free 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 (we add a tiny per-deposit
offset so amounts are unique), the deposit is auto-approved and the user's
wallet is credited.

No API keys are required. APIs used:
  * TRC20: https://api.trongrid.io (public)
  * BEP20: https://api.bscscan.com (free, keyless tier)
  * BTC:   https://mempool.space  (public)
"""
from __future__ import annotations
import asyncio, logging, time
from typing import Optional
import aiohttp

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

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

POLL_INTERVAL = 25         # seconds between polls per chain
EXPIRY_TICK   = 30         # seconds between expiry sweeps
USDT_TRC20_CONTRACT = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"
USDT_BEP20_CONTRACT = "0x55d398326f99059fF775485246999027B3197955"

# Match tolerance — 1 atomic unit either side accounts for rounding.
USDT_DECIMALS = 6
BTC_DECIMALS  = 8


# --------------------------------------------------------------------------
# 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):
    """Mark deposit completed, credit wallet, notify user."""
    # Idempotency guard.
    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
    # Edit the pending message if we have it.
    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
# --------------------------------------------------------------------------

async def _poll_trc20(session: aiohttp.ClientSession, bot):
    if not USDT_TRC20_ADDRESS:
        return
    pending = await _active_deposits("trc20")
    if not pending:
        return
    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}")
    try:
        async with session.get(url, timeout=aiohttp.ClientTimeout(total=15)) as r:
            j = await r.json()
    except Exception as e:
        log.debug("trc20 poll error: %s", e)
        return
    txs = j.get("data") or []
    for tx in txs:
        if (tx.get("to") or "").lower() != USDT_TRC20_ADDRESS.lower():
            continue
        try:
            raw = int(tx.get("value") or 0)
        except Exception:
            continue
        usdt = raw / 10 ** USDT_DECIMALS
        txid = tx.get("transaction_id") or ""
        if not txid or await _txid_seen(txid):
            continue
        # match against any active deposit
        for d in pending:
            if d["status"] == "payment_completed":
                continue
            target = float(d["unique_amount"] or 0)
            if abs(usdt - target) <= 0.000002:  # 2 micro-USDT tolerance
                await _complete_deposit(bot, d, txid, target)
                break


# --------------------------------------------------------------------------
# BEP20 watcher
# --------------------------------------------------------------------------

async def _poll_bep20(session: aiohttp.ClientSession, bot):
    if not USDT_BEP20_ADDRESS:
        return
    pending = await _active_deposits("bep20")
    if not pending:
        return
    url = (f"https://api.bscscan.com/api?module=account&action=tokentx"
           f"&contractaddress={USDT_BEP20_CONTRACT}"
           f"&address={USDT_BEP20_ADDRESS}&sort=desc&page=1&offset=50")
    try:
        async with session.get(url, timeout=aiohttp.ClientTimeout(total=15)) as r:
            j = await r.json()
    except Exception as e:
        log.debug("bep20 poll error: %s", e)
        return
    if str(j.get("status")) != "1":
        return
    for tx in (j.get("result") or []):
        if (tx.get("to") or "").lower() != USDT_BEP20_ADDRESS.lower():
            continue
        try:
            raw = int(tx.get("value") or 0)
        except Exception:
            continue
        usdt = raw / 10 ** USDT_DECIMALS
        txid = tx.get("hash") or ""
        if not txid or await _txid_seen(txid):
            continue
        for d in pending:
            if d["status"] == "payment_completed":
                continue
            target = float(d["unique_amount"] or 0)
            if abs(usdt - target) <= 0.000002:
                await _complete_deposit(bot, d, txid, target)
                break


# --------------------------------------------------------------------------
# BTC watcher
# --------------------------------------------------------------------------

async def _poll_btc(session: aiohttp.ClientSession, bot):
    if not BTC_ADDRESS:
        return
    pending = await _active_deposits("btc")
    if not pending:
        return
    # mempool.space returns recent txs (mempool + confirmed)
    url = f"https://mempool.space/api/address/{BTC_ADDRESS}/txs"
    try:
        async with session.get(url, timeout=aiohttp.ClientTimeout(total=15)) as r:
            txs = await r.json()
    except Exception as e:
        log.debug("btc poll error: %s", e)
        return
    if not isinstance(txs, list):
        return
    for tx in txs[:50]:
        txid = tx.get("txid") or ""
        if not txid:
            continue
        # sum vout to our address
        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) <= 0.00000002:  # 2 sat
                if await _txid_seen(txid):
                    continue
                if d["status"] in ("payment_completed",):
                    continue
                if not confirmed:
                    await _mark_detected(bot, d, txid)
                else:
                    await _complete_deposit(bot, d, txid, float(d["amount"]))
                break


# --------------------------------------------------------------------------
# Expiry sweep
# --------------------------------------------------------------------------

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


# --------------------------------------------------------------------------
# Public runner
# --------------------------------------------------------------------------

_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": "GenBoostBot/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
