import os
import asyncio
import secrets
import sqlite3
import logging
from datetime import datetime, timezone

from dotenv import load_dotenv
from pyrogram import Client, filters
from pyrogram.types import (
    InlineKeyboardMarkup, InlineKeyboardButton, ReplyKeyboardMarkup,
    KeyboardButton, Message
)
from pyrogram.errors import UserNotParticipant, ChatAdminRequired, PeerIdInvalid

load_dotenv()
logging.basicConfig(level=logging.INFO)
log = logging.getLogger("v4bot")

# Required credentials are read from .env (never hard-code them here).
API_ID = int(os.getenv("API_ID", "0"))
API_HASH = os.getenv("API_HASH", "").strip()
BOT_TOKEN = os.getenv("BOT_TOKEN", "").strip()
OWNER_ID = int(os.getenv("OWNER_ID", "0"))
DB_PATH = os.getenv("DB_PATH", "bot.sqlite3").strip() or "bot.sqlite3"
KEY_CHANNEL_URL = os.getenv("KEY_CHANNEL_URL", "").strip()
KEY_TTL_HOURS = max(1, int(os.getenv("KEY_TTL_HOURS", "24")))

missing = []
if not API_ID: missing.append("API_ID")
if not API_HASH: missing.append("API_HASH")
if not BOT_TOKEN: missing.append("BOT_TOKEN")
if not OWNER_ID: missing.append("OWNER_ID")
if missing:
    raise RuntimeError("Missing required .env values: " + ", ".join(missing) +
                       ". Fill these values in .env first.")

# Relative DB paths always resolve beside this script.
if not os.path.isabs(DB_PATH):
    DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), DB_PATH)

app = Client("kurama_bot_session", api_id=API_ID, api_hash=API_HASH, bot_token=BOT_TOKEN)

# ---------- SQLite ----------
def db():
    parent = os.path.dirname(DB_PATH)
    if parent:
        os.makedirs(parent, exist_ok=True)
    con = sqlite3.connect(DB_PATH, timeout=30)
    con.row_factory = sqlite3.Row
    con.execute("PRAGMA foreign_keys = ON")
    con.execute("PRAGMA busy_timeout = 30000")
    return con

def init_db():
    with db() as con:
        con.executescript("""
        CREATE TABLE IF NOT EXISTS users(
            user_id INTEGER PRIMARY KEY, username TEXT, first_name TEXT,
            joined_at TEXT, blocked INTEGER DEFAULT 0
        );
        CREATE TABLE IF NOT EXISTS admins(user_id INTEGER PRIMARY KEY);
        CREATE TABLE IF NOT EXISTS channels(
            chat_id TEXT NOT NULL, title TEXT, kind TEXT NOT NULL,
            PRIMARY KEY(chat_id, kind)
        );
        CREATE TABLE IF NOT EXISTS settings(key TEXT PRIMARY KEY, value TEXT);
        CREATE TABLE IF NOT EXISTS contents(
            kind TEXT PRIMARY KEY, file_id TEXT, caption TEXT DEFAULT ''
        );
        CREATE TABLE IF NOT EXISTS start_buttons(
            id INTEGER PRIMARY KEY AUTOINCREMENT, label TEXT NOT NULL,
            emoji TEXT DEFAULT '🔘', action TEXT DEFAULT 'menu'
        );
        CREATE TABLE IF NOT EXISTS keys(
            token TEXT PRIMARY KEY, user_id INTEGER, created_at TEXT,
            used INTEGER DEFAULT 0
        );
        """)
        # Migrate legacy channels table whose only primary key was chat_id.
        info = con.execute("PRAGMA table_info(channels)").fetchall()
        pk = [r["name"] for r in sorted(info, key=lambda x: x["pk"]) if r["pk"]]
        if pk == ["chat_id"]:
            con.execute("ALTER TABLE channels RENAME TO channels_legacy")
            con.execute("CREATE TABLE channels(chat_id TEXT NOT NULL, title TEXT, kind TEXT NOT NULL, PRIMARY KEY(chat_id, kind))")
            con.execute("INSERT OR IGNORE INTO channels(chat_id,title,kind) SELECT chat_id,title,kind FROM channels_legacy")
            con.execute("DROP TABLE channels_legacy")
        con.execute("INSERT OR IGNORE INTO admins(user_id) VALUES(?)", (OWNER_ID,))
        count = con.execute("SELECT COUNT(*) FROM start_buttons").fetchone()[0]
        if count == 0:
            defaults = [
                ("Photo", "🖼️", "photo"), ("Video", "🎬", "video"),
                ("Voice", "🎤", "voice"), ("Voice Text", "🗣️", "voice_text"),
                ("Caption", "✍️", "caption"), ("Get Key", "🔑", "get_key"),
                ("Updates", "📢", "updates")
            ]
            con.executemany("INSERT INTO start_buttons(label,emoji,action) VALUES(?,?,?)", defaults)

def is_admin(uid):
    with db() as con:
        return con.execute("SELECT 1 FROM admins WHERE user_id=?", (uid,)).fetchone() is not None

def user_count():
    with db() as con:
        return con.execute("SELECT COUNT(*) FROM users").fetchone()[0]

def admin_count():
    with db() as con:
        return con.execute("SELECT COUNT(*) FROM admins").fetchone()[0]

def channel_count(kind):
    with db() as con:
        return con.execute("SELECT COUNT(*) FROM channels WHERE kind=?", (kind,)).fetchone()[0]

def all_user_ids(active_only=False):
    with db() as con:
        q = "SELECT user_id FROM users WHERE blocked=0" if active_only else "SELECT user_id FROM users"
        return [r["user_id"] for r in con.execute(q).fetchall()]

def get_setting(key, default=""):
    with db() as con:
        row = con.execute("SELECT value FROM settings WHERE key=?", (key,)).fetchone()
        return row["value"] if row else default

def set_setting(key, value):
    with db() as con:
        con.execute("INSERT INTO settings(key,value) VALUES(?,?) ON CONFLICT(key) DO UPDATE SET value=excluded.value", (key, value))

def get_buttons():
    with db() as con:
        return con.execute("SELECT * FROM start_buttons ORDER BY id").fetchall()

def get_content(kind):
    with db() as con:
        return con.execute("SELECT * FROM contents WHERE kind=?", (kind,)).fetchone()

def save_content(kind, file_id, caption=""):
    with db() as con:
        con.execute("""INSERT INTO contents(kind,file_id,caption) VALUES(?,?,?)
        ON CONFLICT(kind) DO UPDATE SET file_id=excluded.file_id, caption=excluded.caption""",
        (kind, file_id, caption or ""))

def add_channel(chat_id, title, kind):
    with db() as con:
        con.execute("INSERT OR REPLACE INTO channels(chat_id,title,kind) VALUES(?,?,?)",
                   (str(chat_id), title, kind))

def remove_channel(chat_id, kind):
    with db() as con:
        con.execute("DELETE FROM channels WHERE chat_id=? AND kind=?", (str(chat_id), kind))

def channels(kind=None):
    with db() as con:
        if kind:
            return con.execute("SELECT * FROM channels WHERE kind=?", (kind,)).fetchall()
        return con.execute("SELECT * FROM channels").fetchall()

def add_admin(uid):
    with db() as con:
        con.execute("INSERT OR IGNORE INTO admins(user_id) VALUES(?)", (uid,))

def remove_admin(uid):
    if uid == OWNER_ID:
        return False
    with db() as con:
        con.execute("DELETE FROM admins WHERE user_id=?", (uid,))
    return True

def make_key(uid):
    token = secrets.token_urlsafe(16)
    now = datetime.now(timezone.utc).isoformat()
    with db() as con:
        con.execute("INSERT INTO keys(token,user_id,created_at,used) VALUES(?,?,?,0)", (token, uid, now))
    return token

def key_valid(token, uid):
    with db() as con:
        row = con.execute("SELECT * FROM keys WHERE token=? AND user_id=? AND used=0", (token, uid)).fetchone()
        if not row:
            return False
        try:
            created = datetime.fromisoformat(row["created_at"])
            if created.tzinfo is None:
                created = created.replace(tzinfo=timezone.utc)
        except (TypeError, ValueError):
            return False
        age = (datetime.now(timezone.utc) - created).total_seconds()
        if age > KEY_TTL_HOURS * 3600:
            return False
        con.execute("UPDATE keys SET used=1 WHERE token=?", (token,))
        return True

init_db()

# ---------- keyboards / menu ----------
def admin_keyboard():
    return InlineKeyboardMarkup([
        [InlineKeyboardButton("🖼️ Photo", callback_data="content:photo"),
         InlineKeyboardButton("🎬 Video", callback_data="content:video")],
        [InlineKeyboardButton("🎤 Voice", callback_data="content:voice"),
         InlineKeyboardButton("🗣️ Voice Text", callback_data="content:voice_text")],
        [InlineKeyboardButton("✍️ Caption", callback_data="content:caption"),
         InlineKeyboardButton("🔑 Set Get Key Link", callback_data="set_key_link")],
        [InlineKeyboardButton("🔘 Change Button Emoji", callback_data="change_emoji"),
         InlineKeyboardButton("➕ Add Start Channel", callback_data="add_channel:start")],
        [InlineKeyboardButton("➖ Del Start Channel", callback_data="del_channel:start"),
         InlineKeyboardButton("➕ Add Verify Channel", callback_data="add_channel:verify")],
        [InlineKeyboardButton("➖ Del Verify Channel", callback_data="del_channel:verify")],
        [InlineKeyboardButton("👤 Add Admin", callback_data="add_admin"),
         InlineKeyboardButton("🗑 Remove Admin", callback_data="remove_admin")],
        [InlineKeyboardButton("📢 Broadcast", callback_data="broadcast:all"),
         InlineKeyboardButton("📣 OM Broadcast", callback_data="broadcast:active")],
        [InlineKeyboardButton("🔄 Refresh", callback_data="admin:refresh")]
    ])

def start_markup():
    rows, row = [], []
    for b in get_buttons():
        label = f'{b["emoji"]} {b["label"]}'
        row.append(KeyboardButton(label))
        if len(row) == 2:
            rows.append(row); row=[]
    if row: rows.append(row)
    return ReplyKeyboardMarkup(rows, resize_keyboard=True)

def user_button_action(text):
    for b in get_buttons():
        if text == f'{b["emoji"]} {b["label"]}':
            return b["action"]
    return None

async def user_is_member(uid):
    for ch in channels("verify"):
        try:
            member = await app.get_chat_member(ch["chat_id"], uid)
            if member.status in ("left", "kicked"):
                return False, ch["title"]
        except UserNotParticipant:
            return False, ch["title"]
        except Exception as e:
            log.warning("Membership check %s: %s", ch["chat_id"], e)
            return False, ch["title"]
    return True, ""

async def force_join_msg(chat_id, title):
    buttons = []
    for ch in channels("verify"):
        try:
            chat = await app.get_chat(ch["chat_id"])
            url = chat.invite_link
            if not url:
                url = await app.export_chat_invite_link(ch["chat_id"])
            buttons.append([InlineKeyboardButton(f"Join {ch['title']}", url=url)])
        except Exception:
            pass
    buttons.append([InlineKeyboardButton("✅ I've Joined — Check", callback_data="check_join")])
    await app.send_message(chat_id,
        f"🔒 To use this bot, please join the required channel(s) first.\nThen press **I've Joined — Check**.",
        reply_markup=InlineKeyboardMarkup(buttons))

async def send_saved(chat_id, kind):
    item = get_content(kind)
    if not item:
        await app.send_message(chat_id, "⚠️ This content has not been configured by an admin yet.")
        return
    fid, caption = item["file_id"], item["caption"]
    if kind == "photo":
        await app.send_photo(chat_id, fid, caption=caption or None)
    elif kind == "video":
        await app.send_video(chat_id, fid, caption=caption or None)
    elif kind == "voice":
        await app.send_voice(chat_id, fid, caption=caption or None)
    elif kind == "voice_text":
        await app.send_message(chat_id, caption or "No voice-text message configured.")
    elif kind == "caption":
        await app.send_message(chat_id, caption or "No caption configured.")

async def show_admin(chat_id):
    emoji = get_setting("button_emoji", "🔘")
    await app.send_message(chat_id,
        f"🔥 **Admin Panel**\n\n📊 **Total Users:** {user_count()}\n⭐ **Total Admins:** {admin_count()}/20\n🔘 **Start Buttons:** {len(get_buttons())}\n🛡️ **Verify Channels:** {channel_count('verify')}\n🔑 **Get Key Link:** {get_setting('key_link', 'Not set')}",
        reply_markup=admin_keyboard())

# ---------- conversation state ----------
pending = {}  # uid -> action

@app.on_message(filters.command("start") & filters.private)
async def start_handler(client, message):
    uid = message.from_user.id
    with db() as con:
        con.execute("""INSERT INTO users(user_id,username,first_name,joined_at,blocked)
        VALUES(?,?,?,?,0) ON CONFLICT(user_id) DO UPDATE SET username=excluded.username,
        first_name=excluded.first_name""",
        (uid, message.from_user.username or "", message.from_user.first_name or "",
         datetime.now(timezone.utc).isoformat()))
    arg = message.command[1] if len(message.command) > 1 else ""
    if arg.startswith("key_"):
        token = arg[4:]
        if key_valid(token, uid):
            await message.reply("✅ Key verified successfully!")
            if KEY_CHANNEL_URL:
                await message.reply(f"🔓 Continue here: {KEY_CHANNEL_URL}")
            else:
                await message.reply("🔓 You now have access. Use /start to open the bot menu.")
        else:
            await message.reply("❌ This key is invalid, expired, already used, or belongs to another user.")
        return
    ok, title = await user_is_member(uid)
    if not ok:
        await force_join_msg(message.chat.id, title)
        return
    await message.reply("👋 Welcome! Choose an option below.", reply_markup=start_markup())
    start_chs = channels("start")
    if start_chs:
        join_rows = []
        for ch in start_chs:
            try:
                chat = await app.get_chat(ch["chat_id"])
                url = f"https://t.me/{chat.username}" if chat.username else chat.invite_link
                if not url:
                    url = await app.export_chat_invite_link(chat.id)
                join_rows.append([InlineKeyboardButton(f"📢 {ch['title']}", url=url)])
            except Exception:
                continue
        if join_rows:
            await message.reply("📌 Join our channels:", reply_markup=InlineKeyboardMarkup(join_rows))

@app.on_message(filters.command("admin") & filters.private)
async def admin_cmd(client, message):
    if not is_admin(message.from_user.id):
        return await message.reply("⛔ Admin only.")
    await show_admin(message.chat.id)

@app.on_callback_query(filters.regex("^admin:refresh$"))
async def refresh_cb(client, cq):
    if not is_admin(cq.from_user.id):
        return await cq.answer("Admin only", show_alert=True)
    await cq.answer("Updated")
    await cq.message.edit_text(
        f"🔥 **Admin Panel**\n\n📊 **Total Users:** {user_count()}\n⭐ **Total Admins:** {admin_count()}/20\n🔘 **Start Buttons:** {len(get_buttons())}\n🛡️ **Verify Channels:** {channel_count('verify')}\n🔑 **Get Key Link:** {get_setting('key_link', 'Not set')}",
        reply_markup=admin_keyboard())

@app.on_callback_query(filters.regex("^check_join$"))
async def check_join_cb(client, cq):
    ok, title = await user_is_member(cq.from_user.id)
    if ok:
        await cq.message.reply("✅ Verification complete. Send /start to open menu.")
    else:
        await cq.answer("You have not joined all required channels yet.", show_alert=True)

@app.on_callback_query(filters.regex(r"^content:(photo|video|voice|voice_text|caption)$"))
async def content_cb(client, cq):
    if not is_admin(cq.from_user.id):
        return await cq.answer("Admin only", show_alert=True)
    kind = cq.data.split(":", 1)[1]
    pending[cq.from_user.id] = f"save:{kind}"
    prompts = {
        "photo": "Send the photo to save as the bot's Photo content.",
        "video": "Send the video to save as the bot's Video content.",
        "voice": "Send a voice message to save as the bot's Voice content.",
        "voice_text": "Send the text to save as the Voice Text content.",
        "caption": "Send the caption/text to save as the Caption content."
    }
    await cq.message.reply(prompts[kind] + "\n/cancel to stop.")

@app.on_callback_query(filters.regex("^set_key_link$"))
async def set_key_link_cb(client, cq):
    if not is_admin(cq.from_user.id):
        return await cq.answer("Admin only", show_alert=True)
    pending[cq.from_user.id] = "set_key_link"
    await cq.message.reply("Send the destination URL for the Get Key system (or `OFF`).\n/cancel to stop.")

@app.on_callback_query(filters.regex("^change_emoji$"))
async def change_emoji_cb(client, cq):
    if not is_admin(cq.from_user.id):
        return await cq.answer("Admin only", show_alert=True)
    pending[cq.from_user.id] = "change_emoji"
    await cq.message.reply("Send one emoji to use before all Start menu buttons.\n/cancel to stop.")

@app.on_callback_query(filters.regex(r"^add_channel:(start|verify)$"))
async def add_channel_cb(client, cq):
    if not is_admin(cq.from_user.id):
        return await cq.answer("Admin only", show_alert=True)
    kind = cq.data.split(":")[1]
    pending[cq.from_user.id] = f"add_channel:{kind}"
    await cq.message.reply("Send the channel @username or numeric chat ID. Make the bot an admin in the channel first.\n/cancel to stop.")

@app.on_callback_query(filters.regex(r"^del_channel:(start|verify)$"))
async def del_channel_cb(client, cq):
    if not is_admin(cq.from_user.id):
        return await cq.answer("Admin only", show_alert=True)
    kind = cq.data.split(":")[1]
    rows = channels(kind)
    if not rows:
        return await cq.message.reply("No channels configured.")
    buttons = [[InlineKeyboardButton(f"🗑 {r['title']} ({r['chat_id']})", callback_data=f"delch:{kind}:{r['chat_id']}")] for r in rows]
    await cq.message.reply("Select a channel to delete:", reply_markup=InlineKeyboardMarkup(buttons))

@app.on_callback_query(filters.regex(r"^delch:(start|verify):"))
async def delch_cb(client, cq):
    if not is_admin(cq.from_user.id):
        return await cq.answer("Admin only", show_alert=True)
    _, kind, chat_id = cq.data.split(":", 2)
    remove_channel(chat_id, kind)
    await cq.answer("Channel deleted")
    await cq.message.reply("Deleted.")

@app.on_callback_query(filters.regex("^add_admin$"))
async def add_admin_cb(client, cq):
    if not is_admin(cq.from_user.id):
        return await cq.answer("Admin only", show_alert=True)
    if admin_count() >= 20:
        return await cq.message.reply("Admin limit (20) reached.")
    pending[cq.from_user.id] = "add_admin"
    await cq.message.reply("Send the Telegram numeric user ID to add as admin.\n/cancel to stop.")

@app.on_callback_query(filters.regex("^remove_admin$"))
async def remove_admin_cb(client, cq):
    if not is_admin(cq.from_user.id):
        return await cq.answer("Admin only", show_alert=True)
    with db() as con:
        rows = con.execute("SELECT user_id FROM admins WHERE user_id!=?", (OWNER_ID,)).fetchall()
    if not rows:
        return await cq.message.reply("No removable admins.")
    buttons = [[InlineKeyboardButton(str(r["user_id"]), callback_data=f"rmadmin:{r['user_id']}")] for r in rows]
    await cq.message.reply("Choose admin to remove:", reply_markup=InlineKeyboardMarkup(buttons))

@app.on_callback_query(filters.regex("^rmadmin:"))
async def rmadmin_cb(client, cq):
    if not is_admin(cq.from_user.id):
        return await cq.answer("Admin only", show_alert=True)
    uid = int(cq.data.split(":")[1])
    remove_admin(uid)
    await cq.answer("Admin removed")
    await cq.message.reply(f"Removed admin {uid}.")

@app.on_callback_query(filters.regex(r"^broadcast:(all|active)$"))
async def broadcast_cb(client, cq):
    if not is_admin(cq.from_user.id):
        return await cq.answer("Admin only", show_alert=True)
    mode = cq.data.split(":")[1]
    pending[cq.from_user.id] = f"broadcast:{mode}"
    await cq.message.reply(
        ("Send the message/media to broadcast to **all registered users**." if mode == "all"
         else "Send the message/media for **OM Broadcast (active users only)**.") + "\n/cancel to stop.")

@app.on_message(filters.private & filters.text)
async def text_router(client, message):
    uid = message.from_user.id
    txt = message.text.strip()
    if txt == "/cancel":
        pending.pop(uid, None)
        return await message.reply("Cancelled.")
    if uid in pending and is_admin(uid):
        action = pending.pop(uid)
        if action.startswith("save:"):
            kind = action.split(":")[1]
            save_content(kind, "", txt) if kind in ("voice_text", "caption") else None
            if kind in ("voice_text", "caption"):
                await message.reply(f"✅ Saved {kind}.")
            else:
                await message.reply("⚠️ Please send the required media type, not text. Try again with /admin.")
            return
        if action == "set_key_link":
            set_setting("key_link", "" if txt.upper() == "OFF" else txt)
            await message.reply("✅ Get Key destination updated.")
            return
        if action == "change_emoji":
            set_setting("button_emoji", txt)
            with db() as con:
                con.execute("UPDATE start_buttons SET emoji=?", (txt,))
            await message.reply("✅ Button emoji updated.")
            return
        if action.startswith("add_channel:"):
            kind = action.split(":")[1]
            try:
                chat = await app.get_chat(txt)
                # Ensure bot is administrator; invite link access may need admin permissions.
                bot_me = await app.get_me()
                member = await app.get_chat_member(chat.id, bot_me.id)
                if member.status not in ("administrator", "owner"):
                    return await message.reply("❌ Make this bot an admin in the channel, then retry.")
                add_channel(chat.id, chat.title, kind)
                await message.reply(f"✅ Added {chat.title} as {kind} channel.")
            except Exception as e:
                await message.reply(f"❌ Couldn't add channel. Check the @username/ID and bot admin rights.\n{e}")
            return
        if action == "add_admin":
            try:
                new_uid = int(txt)
                if new_uid == OWNER_ID:
                    return await message.reply("Owner is already admin.")
                add_admin(new_uid)
                await message.reply(f"✅ Added admin {new_uid}.")
            except ValueError:
                await message.reply("Send a numeric Telegram user ID.")
            return
        if action.startswith("broadcast:"):
            mode = action.split(":")[1]
            recipients = all_user_ids(active_only=(mode == "active"))
            sent = failed = 0
            for target in recipients:
                try:
                    await message.copy(target)
                    sent += 1
                except Exception:
                    failed += 1
                    with db() as con:
                        con.execute("UPDATE users SET blocked=1 WHERE user_id=?", (target,))
                await asyncio.sleep(0.04)
            await message.reply(f"📢 Broadcast finished.\n✅ Sent: {sent}\n❌ Failed: {failed}")
            return

    if txt.startswith("/"):
        return
    ok, _ = await user_is_member(uid)
    if not ok:
        await force_join_msg(message.chat.id, "")
        return
    action = user_button_action(txt)
    if action:
        if action == "get_key":
            token = make_key(uid)
            me = await app.get_me()
            deep_link = f"https://t.me/{me.username}?start=key_{token}"
            await message.reply(f"🔑 Your personal key link (valid {KEY_TTL_HOURS} hours, one use):\n{deep_link}")
        elif action == "updates":
            url = get_setting("key_link", "")
            await message.reply(f"📢 Updates / access link:\n{url or 'No link configured yet.'}")
        else:
            await send_saved(message.chat.id, action)
        return
    await message.reply("Choose an option from the menu below.", reply_markup=start_markup())

# Admin can send media to pending content save / broadcast.
@app.on_message(filters.private & (filters.photo | filters.video | filters.voice | filters.audio | filters.document))
async def media_router(client, message):
    uid = message.from_user.id
    if uid not in pending or not is_admin(uid):
        return
    action = pending.pop(uid)
    if action.startswith("save:"):
        kind = action.split(":")[1]
        media = None
        if kind == "photo" and message.photo:
            media = message.photo.file_id
        elif kind == "video" and message.video:
            media = message.video.file_id
        elif kind == "voice" and message.voice:
            media = message.voice.file_id
        if media:
            save_content(kind, media, message.caption or "")
            await message.reply(f"✅ Saved {kind} content.")
        else:
            pending[uid] = action
            await message.reply("❌ Wrong media type. Please send the requested media.")
    elif action.startswith("broadcast:"):
        mode = action.split(":")[1]
        recipients = all_user_ids(active_only=(mode == "active"))
        sent = failed = 0
        for target in recipients:
            try:
                await message.copy(target)
                sent += 1
            except Exception:
                failed += 1
                with db() as con:
                    con.execute("UPDATE users SET blocked=1 WHERE user_id=?", (target,))
            await asyncio.sleep(0.04)
        await message.reply(f"📢 Broadcast finished.\n✅ Sent: {sent}\n❌ Failed: {failed}")

@app.on_message(filters.private & filters.command("stats"))
async def stats_cmd(client, message):
    if not is_admin(message.from_user.id):
        return
    await show_admin(message.chat.id)

if __name__ == "__main__":
    log.info("Kurama bot is starting...")
    try:
        app.run()
    except KeyboardInterrupt:
        log.info("Bot stopped by user.")
    except Exception:
        log.exception("Bot startup/runtime failed.")
        raise
