# -*- coding: utf-8 -*-
"""
لایه دیتابیس ربات ممبرگیر
=============================
همه‌ی تعامل‌ها با دیتابیس SQLite از این فایل رد میشه.
یک Lock سراسری داریم چون pyTelegramBotAPI با threaded=True
برای هر پیام یک ترد جدا باز میکنه و SQLite با چند ترد همزمان
مشکل داره.
"""

import random
import sqlite3
import string
import threading
import time

import config
import texts as texts_registry

_lock = threading.RLock()
_conn = sqlite3.connect(config.DB_PATH, check_same_thread=False)
_conn.row_factory = sqlite3.Row
# WAL باعث میشه خوندن و نوشتن هم‌زمان روی دیتابیس بهتر جواب بده - مهم
# برای وقتی که تعداد کاربرها به چند هزار نفر برسه و درخواست‌ها زیاد بشن.
_conn.execute("PRAGMA journal_mode=WAL")
_conn.execute("PRAGMA synchronous=NORMAL")


_ORDER_CODE_ALPHABET = string.ascii_uppercase + string.digits


def _generate_unique_order_code(cur):
    """یه کد ۸ کاراکتری تصادفی (حروف بزرگ+عدد) میسازه که توی جدول campaigns تکراری نباشه."""
    for _ in range(30):
        code = "".join(random.choices(_ORDER_CODE_ALPHABET, k=8))
        cur.execute("SELECT 1 FROM campaigns WHERE order_code=?", (code,))
        if not cur.fetchone():
            return code
    # اگه به هر دلیل عجیبی ۳۰ بار همه تکراری بودن (عملا غیرممکنه)، یه کد بلندتر بساز
    return "".join(random.choices(_ORDER_CODE_ALPHABET, k=12))


# ---------------------------------------------------------------------------
# ساخت جدول‌ها
# ---------------------------------------------------------------------------
def init_db():
    with _lock:
        cur = _conn.cursor()
        cur.executescript(
            """
            CREATE TABLE IF NOT EXISTS users (
                user_id      INTEGER PRIMARY KEY,
                username     TEXT,
                first_name   TEXT,
                coins        INTEGER NOT NULL DEFAULT 0,
                referred_by  INTEGER,
                is_banned    INTEGER NOT NULL DEFAULT 0,
                joined_at    INTEGER NOT NULL,
                last_daily   INTEGER NOT NULL DEFAULT 0
            );

            CREATE TABLE IF NOT EXISTS tickets (
                id           INTEGER PRIMARY KEY AUTOINCREMENT,
                user_id      INTEGER NOT NULL,
                message      TEXT,
                status       TEXT NOT NULL DEFAULT 'open',   -- open / answered
                created_at   INTEGER NOT NULL
            );

            CREATE TABLE IF NOT EXISTS admins (
                user_id  INTEGER PRIMARY KEY,
                added_at INTEGER NOT NULL
            );

            CREATE TABLE IF NOT EXISTS settings (
                key   TEXT PRIMARY KEY,
                value TEXT
            );

            CREATE TABLE IF NOT EXISTS force_channels (
                id           INTEGER PRIMARY KEY AUTOINCREMENT,
                channel_id   TEXT NOT NULL UNIQUE,   -- @username یا -100xxxxxxxxxx
                title        TEXT,
                channel_link TEXT
            );

            CREATE TABLE IF NOT EXISTS campaigns (
                id              INTEGER PRIMARY KEY AUTOINCREMENT,
                owner_id        INTEGER NOT NULL,
                channel_id      TEXT NOT NULL,     -- @username یا آیدی عددی
                channel_title   TEXT,
                channel_link    TEXT,
                total_coins     INTEGER NOT NULL,
                remaining       INTEGER NOT NULL,
                status          TEXT NOT NULL DEFAULT 'active',  -- active / finished / stopped
                created_at      INTEGER NOT NULL,
                order_code      TEXT UNIQUE,       -- کد رهگیری کوتاه و یکتا برای پیگیری سفارش
                log_message_id  INTEGER            -- آیدی پیامی که توی کانال لاگ ازش پست شده
            );

            CREATE TABLE IF NOT EXISTS campaign_joins (
                id           INTEGER PRIMARY KEY AUTOINCREMENT,
                campaign_id  INTEGER NOT NULL,
                user_id      INTEGER NOT NULL,
                joined_at    INTEGER NOT NULL,
                UNIQUE(campaign_id, user_id)
            );

            CREATE TABLE IF NOT EXISTS payments (
                id           INTEGER PRIMARY KEY AUTOINCREMENT,
                user_id      INTEGER NOT NULL,
                coins        INTEGER NOT NULL,
                amount_toman INTEGER NOT NULL,
                receipt_file TEXT,           -- file_id عکس فیش واریزی
                status       TEXT NOT NULL DEFAULT 'pending',  -- pending / approved / rejected
                created_at   INTEGER NOT NULL,
                reviewed_by  INTEGER,
                reviewed_at  INTEGER
            );

            CREATE TABLE IF NOT EXISTS menu_buttons (
                id           INTEGER PRIMARY KEY AUTOINCREMENT,
                button_key   TEXT NOT NULL UNIQUE,  -- کلید داخلی ثابت (wallet/earn/...) یا custom_<ts>
                label        TEXT NOT NULL,          -- متنی که روی دکمه دیده میشه (قابل تغییر)
                enabled      INTEGER NOT NULL DEFAULT 1,
                position     INTEGER NOT NULL DEFAULT 0,
                kind         TEXT NOT NULL DEFAULT 'core',  -- core / custom
                custom_text  TEXT                    -- فقط برای دکمه‌های سفارشی
            );

            CREATE TABLE IF NOT EXISTS texts (
                key   TEXT PRIMARY KEY,
                value TEXT NOT NULL
            );

            CREATE TABLE IF NOT EXISTS channel_earnings (
                id         INTEGER PRIMARY KEY AUTOINCREMENT,
                user_id    INTEGER NOT NULL,
                channel_id TEXT NOT NULL,
                earned_at  INTEGER NOT NULL,
                left_at    INTEGER,
                UNIQUE(user_id, channel_id)
            );
            """
        )
        _conn.commit()

        # مهاجرت نرم برای دیتابیس‌های قدیمی‌تر (که قبل از این آپدیت ساخته شدن):
        # اگه ستونی از قبل وجود داشته باشه، خطاش رو نادیده میگیریم تا داده‌های
        # قبلی کاربرها (کوین، تاریخ عضویت و ...) پاک نشه.
        for alter_sql in (
            "ALTER TABLE users ADD COLUMN last_daily INTEGER NOT NULL DEFAULT 0",
            "ALTER TABLE channel_earnings ADD COLUMN left_at INTEGER",
            "ALTER TABLE force_channels ADD COLUMN channel_link TEXT",
            "ALTER TABLE campaigns ADD COLUMN order_code TEXT",
            "ALTER TABLE campaigns ADD COLUMN log_message_id INTEGER",
        ):
            try:
                cur.execute(alter_sql)
                _conn.commit()
            except sqlite3.OperationalError:
                pass

        # ایندکس‌ها - برای این‌که با رشد تعداد کاربر و کمپین (چند هزار به
        # بالا)، کوئری‌های پرتکرار (چک کوین گرفتن، لیست کمپین‌های یه نفر،
        # پرداخت‌های در انتظار و ...) کند نشن. ساختنشون بی‌خطره و اگه از
        # قبل وجود داشته باشن، هیچ کاری نمیکنن.
        cur.executescript(
            """
            CREATE INDEX IF NOT EXISTS idx_campaign_joins_user ON campaign_joins(user_id);
            CREATE INDEX IF NOT EXISTS idx_campaigns_owner ON campaigns(owner_id);
            CREATE INDEX IF NOT EXISTS idx_campaigns_channel ON campaigns(channel_id);
            CREATE INDEX IF NOT EXISTS idx_campaigns_status ON campaigns(status);
            CREATE INDEX IF NOT EXISTS idx_payments_status ON payments(status);
            CREATE INDEX IF NOT EXISTS idx_payments_user ON payments(user_id);
            CREATE INDEX IF NOT EXISTS idx_users_referred_by ON users(referred_by);
            CREATE INDEX IF NOT EXISTS idx_tickets_status ON tickets(status);
            """
        )
        _conn.commit()

        # مهاجرت: به کمپین‌های قدیمی که از قبل ساخته شدن (قبل از قابلیت
        # کد سفارش)، یه کد یکتا اختصاص میدیم تا اون‌ها هم قابل پیگیری بشن
        cur.execute("SELECT id FROM campaigns WHERE order_code IS NULL")
        for row in cur.fetchall():
            code = _generate_unique_order_code(cur)
            cur.execute("UPDATE campaigns SET order_code=? WHERE id=?", (code, row["id"]))
        _conn.commit()

        # مقادیر پیش‌فرض تنظیمات - فقط اگه وجود نداشته باشن اضافه میشن
        defaults = {
            "coin_per_join": "1",          # هر جوین موفق چند کوین بده
            "coins_unit": "10",            # واحد کوین برای قیمت‌گذاری (هر ۱۰ کوین)
            "price_per_unit": "4000",      # قیمت هر واحد (تومان)
            "min_purchase_coins": "50",    # حداقل خرید کوین
            "card_number": "",             # شماره کارت ادمین
            "card_holder": "",             # نام صاحب کارت
            "log_channel_id": "",          # کانالی که خریدهای جدید توش پست میشه
            "welcome_message": (
                "🎉 به ربات ممبرگیر خوش اومدی!\n\n"
                "با عضویت توی کانال‌های تبلیغ‌شده کوین جمع کن، بعد از کوینت "
                "برای گرفتن ممبر واقعی و فعال برای کانال خودت استفاده کن.\n\n"
                "برای شروع از دکمه‌های پایین استفاده کن 👇"
            ),
            "referral_bonus": "2",         # کوین هدیه به دعوت‌کننده
            "daily_bonus": "3",            # کوین پاداش روزانه
            "support_username": "",        # آیدی پشتیبانی (مثلا @support)
        }
        for k, v in defaults.items():
            cur.execute(
                "INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)", (k, v)
            )
        _conn.commit()

        # ادمین اصلی رو از config اضافه کن
        cur.execute(
            "INSERT OR IGNORE INTO admins (user_id, added_at) VALUES (?, ?)",
            (config.OWNER_ID, int(time.time())),
        )
        _conn.commit()

        # دکمه‌های پیش‌فرض منو (فقط بار اول ساخته میشن، بعدش دست ادمینه)
        default_buttons = [
            ("wallet", "💰 کیف پول من", 1),
            ("earn", "🎯 کسب کوین", 2),
            ("campaign", "📢 تبلیغ کانال من", 3),
            ("buy", "💳 خرید کوین", 4),
            ("daily", "🎁 پاداش روزانه", 5),
            ("mystats", "📊 آمار من", 6),
            ("referral", "🔗 لینک دعوت من", 7),
            ("help", "🔍 پیگیری سفارش", 8),
            ("support", "📞 پشتیبانی", 9),
        ]
        for key, label, pos in default_buttons:
            cur.execute(
                """INSERT OR IGNORE INTO menu_buttons
                   (button_key, label, enabled, position, kind)
                   VALUES (?, ?, 1, ?, 'core')""",
                (key, label, pos),
            )
        _conn.commit()

        # مهاجرت نرم: اگه ادمین دکمه‌ی پشتیبانی رو دست نزده و هنوز برچسب
        # قدیمی روشه، به برچسب جدید آپدیتش کن (اگه خودش عوضش کرده، دست نمیزنیم)
        cur.execute(
            "UPDATE menu_buttons SET label='📞 پشتیبانی' "
            "WHERE button_key='support' AND label='🆘 پشتیبانی'"
        )
        _conn.commit()

        # مهاجرت: دکمه‌ی راهنمای قدیمی رو به «پیگیری سفارش» تبدیل کن (فقط
        # اگه ادمین خودش دستی عوضش نکرده باشه - همون قانون همیشگی)
        cur.execute(
            "UPDATE menu_buttons SET label='🔍 پیگیری سفارش' "
            "WHERE button_key='help' AND label='ℹ️ راهنما'"
        )
        _conn.commit()

        # مهاجرت یک‌بارهٔ دیگه: دکمه‌ی پشتیبانی رو ببر ته منو (پایین‌ترین
        # جایگاه) - فقط یک‌بار انجام میشه تا اگه بعداً خود ادمین دستی
        # جابه‌جاش کرد، دوباره برنگردونیمش سرجاش.
        if not is_text_customized("__migrated_support_to_bottom__"):
            cur.execute("SELECT id FROM menu_buttons WHERE button_key='support'")
            support_row = cur.fetchone()
            if support_row:
                cur.execute("SELECT COALESCE(MAX(position), 0) m FROM menu_buttons")
                max_pos = cur.fetchone()["m"]
                cur.execute(
                    "UPDATE menu_buttons SET position=? WHERE id=?",
                    (max_pos + 1, support_row["id"]),
                )
            cur.execute(
                "INSERT INTO texts (key, value) VALUES ('__migrated_support_to_bottom__', '1') "
                "ON CONFLICT(key) DO NOTHING"
            )
            _conn.commit()


# ---------------------------------------------------------------------------
# کاربران
# ---------------------------------------------------------------------------
def get_user(user_id):
    with _lock:
        cur = _conn.cursor()
        cur.execute("SELECT * FROM users WHERE user_id=?", (user_id,))
        return cur.fetchone()


def create_user(user_id, username, first_name, referred_by=None):
    with _lock:
        cur = _conn.cursor()
        cur.execute(
            """INSERT OR IGNORE INTO users
               (user_id, username, first_name, coins, referred_by, joined_at)
               VALUES (?, ?, ?, 0, ?, ?)""",
            (user_id, username, first_name, referred_by, int(time.time())),
        )
        _conn.commit()


def update_username(user_id, username, first_name):
    with _lock:
        cur = _conn.cursor()
        cur.execute(
            "UPDATE users SET username=?, first_name=? WHERE user_id=?",
            (username, first_name, user_id),
        )
        _conn.commit()


def add_coins(user_id, amount):
    with _lock:
        cur = _conn.cursor()
        cur.execute(
            "UPDATE users SET coins = coins + ? WHERE user_id=?", (amount, user_id)
        )
        _conn.commit()


def get_coins(user_id):
    u = get_user(user_id)
    return u["coins"] if u else 0


def deduct_coins(user_id, amount):
    """اگه موجودی کافی بود کسر میکنه و True برمیگردونه، وگرنه False."""
    with _lock:
        cur = _conn.cursor()
        cur.execute("SELECT coins FROM users WHERE user_id=?", (user_id,))
        row = cur.fetchone()
        if not row or row["coins"] < amount:
            return False
        cur.execute(
            "UPDATE users SET coins = coins - ? WHERE user_id=?", (amount, user_id)
        )
        _conn.commit()
        return True


def ban_user(user_id, banned=1):
    with _lock:
        cur = _conn.cursor()
        cur.execute("UPDATE users SET is_banned=? WHERE user_id=?", (banned, user_id))
        _conn.commit()


def is_banned(user_id):
    u = get_user(user_id)
    return bool(u["is_banned"]) if u else False


def all_user_ids():
    with _lock:
        cur = _conn.cursor()
        cur.execute("SELECT user_id FROM users")
        return [r["user_id"] for r in cur.fetchall()]


def user_count():
    with _lock:
        cur = _conn.cursor()
        cur.execute("SELECT COUNT(*) c FROM users")
        return cur.fetchone()["c"]


def get_referrer(user_id):
    u = get_user(user_id)
    if not u or not u["referred_by"]:
        return None
    return get_user(u["referred_by"])


def set_coins(user_id, amount):
    """موجودی کاربر رو دقیقا برابر amount قرار میده (نه اضافه/کم، جایگزین کامل)."""
    with _lock:
        cur = _conn.cursor()
        cur.execute("UPDATE users SET coins=? WHERE user_id=?", (max(0, amount), user_id))
        _conn.commit()


def bulk_add_coins(amount):
    """به همه‌ی کاربرها هم‌زمان amount کوین اضافه میکنه. تعداد کاربرهای تحت تاثیر رو برمیگردونه."""
    with _lock:
        cur = _conn.cursor()
        cur.execute("UPDATE users SET coins = coins + ?", (amount,))
        _conn.commit()
        return cur.rowcount


def bulk_deduct_coins(amount):
    """از همه‌ی کاربرها هم‌زمان amount کوین کم میکنه (هیچ‌وقت منفی نمیشه)."""
    with _lock:
        cur = _conn.cursor()
        cur.execute("UPDATE users SET coins = MAX(coins - ?, 0)", (amount,))
        _conn.commit()
        return cur.rowcount


def find_user(query):
    """
    جستجوی کاربر برای پنل ادمین.
    قبول میکنه: آیدی عددی، یا بخشی از یوزرنیم/نام.
    """
    with _lock:
        cur = _conn.cursor()
        query = query.strip().lstrip("@")
        if query.isdigit():
            cur.execute("SELECT * FROM users WHERE user_id=?", (int(query),))
            row = cur.fetchone()
            if row:
                return row
        cur.execute(
            "SELECT * FROM users WHERE username LIKE ? OR first_name LIKE ? LIMIT 1",
            (f"%{query}%", f"%{query}%"),
        )
        return cur.fetchone()


def get_leaderboard(limit=10):
    with _lock:
        cur = _conn.cursor()
        cur.execute(
            "SELECT user_id, username, first_name, coins FROM users "
            "ORDER BY coins DESC LIMIT ?",
            (limit,),
        )
        return cur.fetchall()


def get_join_count(user_id):
    """تعداد دفعاتی که این کاربر با عضویت در کانال‌ها کوین گرفته."""
    with _lock:
        cur = _conn.cursor()
        cur.execute("SELECT COUNT(*) c FROM campaign_joins WHERE user_id=?", (user_id,))
        return cur.fetchone()["c"]


def get_referral_count(user_id):
    with _lock:
        cur = _conn.cursor()
        cur.execute("SELECT COUNT(*) c FROM users WHERE referred_by=?", (user_id,))
        return cur.fetchone()["c"]


# ---------------------------------------------------------------------------
# پاداش روزانه
# ---------------------------------------------------------------------------
DAILY_COOLDOWN_SECONDS = 24 * 60 * 60


def daily_seconds_left(user_id):
    """اگه هنوز نوبت پاداش روزانه نرسیده، ثانیه‌های باقی‌مونده رو برمیگردونه؛ وگرنه 0."""
    u = get_user(user_id)
    if not u:
        return 0
    last = u["last_daily"] or 0
    passed = int(time.time()) - last
    remaining = DAILY_COOLDOWN_SECONDS - passed
    return max(0, remaining)


def claim_daily(user_id, amount):
    """اگه نوبت رسیده بود کوین رو اضافه و زمان رو ثبت میکنه و True برمیگردونه."""
    with _lock:
        if daily_seconds_left(user_id) > 0:
            return False
        cur = _conn.cursor()
        cur.execute(
            "UPDATE users SET coins = coins + ?, last_daily = ? WHERE user_id=?",
            (amount, int(time.time()), user_id),
        )
        _conn.commit()
        return True


# ---------------------------------------------------------------------------
# ادمین‌ها
# ---------------------------------------------------------------------------
def is_admin(user_id):
    with _lock:
        cur = _conn.cursor()
        cur.execute("SELECT 1 FROM admins WHERE user_id=?", (user_id,))
        return cur.fetchone() is not None


def add_admin(user_id):
    with _lock:
        cur = _conn.cursor()
        cur.execute(
            "INSERT OR IGNORE INTO admins (user_id, added_at) VALUES (?, ?)",
            (user_id, int(time.time())),
        )
        _conn.commit()


def remove_admin(user_id):
    with _lock:
        cur = _conn.cursor()
        cur.execute("DELETE FROM admins WHERE user_id=?", (user_id,))
        _conn.commit()


def list_admins():
    with _lock:
        cur = _conn.cursor()
        cur.execute("SELECT user_id FROM admins")
        return [r["user_id"] for r in cur.fetchall()]


# ---------------------------------------------------------------------------
# تنظیمات (settings) - همه چیز از اینجا توسط ادمین قابل تغییره
# ---------------------------------------------------------------------------
def get_setting(key, default=""):
    with _lock:
        cur = _conn.cursor()
        cur.execute("SELECT value FROM settings WHERE key=?", (key,))
        row = cur.fetchone()
        return row["value"] if row else default


def set_setting(key, value):
    with _lock:
        cur = _conn.cursor()
        cur.execute(
            "INSERT INTO settings (key, value) VALUES (?, ?) "
            "ON CONFLICT(key) DO UPDATE SET value=excluded.value",
            (key, str(value)),
        )
        _conn.commit()


def get_int_setting(key, default=0):
    try:
        return int(get_setting(key, str(default)))
    except (TypeError, ValueError):
        return default


# ---------------------------------------------------------------------------
# کانال‌های اجباری (Force Join)
# ---------------------------------------------------------------------------
def add_force_channel(channel_id, title="", channel_link=""):
    with _lock:
        cur = _conn.cursor()
        cur.execute(
            "INSERT OR IGNORE INTO force_channels (channel_id, title, channel_link) VALUES (?, ?, ?)",
            (channel_id, title, channel_link),
        )
        _conn.commit()


def remove_force_channel(channel_id):
    with _lock:
        cur = _conn.cursor()
        cur.execute("DELETE FROM force_channels WHERE channel_id=?", (channel_id,))
        _conn.commit()


def list_force_channels():
    with _lock:
        cur = _conn.cursor()
        cur.execute("SELECT * FROM force_channels")
        return cur.fetchall()


# ---------------------------------------------------------------------------
# کمپین‌های تبلیغاتی (کانال‌هایی که برای گرفتن ممبر ثبت شدن)
# ---------------------------------------------------------------------------
def create_campaign(owner_id, channel_id, channel_title, channel_link, total_coins):
    with _lock:
        cur = _conn.cursor()
        order_code = _generate_unique_order_code(cur)
        cur.execute(
            """INSERT INTO campaigns
               (owner_id, channel_id, channel_title, channel_link,
                total_coins, remaining, status, created_at, order_code)
               VALUES (?, ?, ?, ?, ?, ?, 'active', ?, ?)""",
            (
                owner_id,
                channel_id,
                channel_title,
                channel_link,
                total_coins,
                total_coins,
                int(time.time()),
                order_code,
            ),
        )
        _conn.commit()
        return cur.lastrowid


def get_campaign_by_order_code(code):
    with _lock:
        cur = _conn.cursor()
        cur.execute("SELECT * FROM campaigns WHERE order_code=?", (code,))
        return cur.fetchone()


def set_campaign_log_message(campaign_id, message_id):
    with _lock:
        cur = _conn.cursor()
        cur.execute("UPDATE campaigns SET log_message_id=? WHERE id=?", (message_id, campaign_id))
        _conn.commit()


def get_campaign(campaign_id):
    with _lock:
        cur = _conn.cursor()
        cur.execute("SELECT * FROM campaigns WHERE id=?", (campaign_id,))
        return cur.fetchone()


def list_active_campaigns(exclude_user=None, limit=10):
    with _lock:
        cur = _conn.cursor()
        if exclude_user is not None:
            cur.execute(
                """SELECT * FROM campaigns
                   WHERE status='active' AND remaining>0 AND owner_id != ?
                   ORDER BY created_at DESC LIMIT ?""",
                (exclude_user, limit),
            )
        else:
            cur.execute(
                """SELECT * FROM campaigns
                   WHERE status='active' AND remaining>0
                   ORDER BY created_at DESC LIMIT ?""",
                (limit,),
            )
        return cur.fetchall()


def list_available_campaigns_for_user(user_id, limit=500):
    """
    نسخه‌ی بهینه‌شده‌ی list_active_campaigns مخصوص صفحه‌ی «کسب کوین»:
    همه‌ی فیلترها (فعال بودن، مال خود کاربر نبودن، قبلاً ازش کوین نگرفته
    بودن - نه از این کمپین، نه از این کانال توی یه کمپین دیگه) توی یک
    کوئری انجام میشه، به‌جای این‌که برای هر کمپین جدا-جدا (N+1) کوئری
    بزنیم. برای وقتی که کمپین‌های فعال و کاربرها هر دو زیاد بشن، خیلی
    سریع‌تره.
    """
    with _lock:
        cur = _conn.cursor()
        cur.execute(
            """
            SELECT campaigns.* FROM campaigns
            WHERE campaigns.status='active' AND campaigns.remaining>0
              AND campaigns.owner_id != ?
              AND NOT EXISTS (
                  SELECT 1 FROM campaign_joins
                  WHERE campaign_joins.campaign_id = campaigns.id
                    AND campaign_joins.user_id = ?
              )
              AND NOT EXISTS (
                  SELECT 1 FROM channel_earnings
                  WHERE channel_earnings.channel_id = campaigns.channel_id
                    AND channel_earnings.user_id = ?
              )
            ORDER BY campaigns.created_at DESC
            LIMIT ?
            """,
            (user_id, user_id, user_id, limit),
        )
        return cur.fetchall()


def has_joined_campaign(campaign_id, user_id):
    with _lock:
        cur = _conn.cursor()
        cur.execute(
            "SELECT 1 FROM campaign_joins WHERE campaign_id=? AND user_id=?",
            (campaign_id, user_id),
        )
        return cur.fetchone() is not None


def record_campaign_join(campaign_id, user_id):
    """
    یک جوین جدید ثبت میکنه و از remaining کم میکنه.
    برمیگردونه: False اگه تکراری بود، وگرنه dict شامل
    {"finished": bool} که میگه آیا کمپین همین الان تکمیل شد یا نه
    (برای اطلاع‌رسانی به صاحب کمپین کاربردیه).
    """
    with _lock:
        cur = _conn.cursor()
        try:
            cur.execute(
                "INSERT INTO campaign_joins (campaign_id, user_id, joined_at) VALUES (?, ?, ?)",
                (campaign_id, user_id, int(time.time())),
            )
        except sqlite3.IntegrityError:
            return False
        cur.execute(
            """UPDATE campaigns SET remaining = remaining - 1
               WHERE id=? AND remaining > 0""",
            (campaign_id,),
        )
        cur.execute("SELECT remaining FROM campaigns WHERE id=?", (campaign_id,))
        remaining = cur.fetchone()["remaining"]
        finished = remaining <= 0
        if finished:
            cur.execute(
                "UPDATE campaigns SET status='finished' WHERE id=?", (campaign_id,)
            )
        _conn.commit()
        return {"finished": finished}


# ---------------------------------------------------------------------------
# جلوگیری از گرفتن چندبارهٔ کوین از یک کانال (حتی اگه توی چند کمپین جدا
# تبلیغ شده باشه) - هر کاربر فقط یک‌بار از هر کانال کوین میگیره
# ---------------------------------------------------------------------------
def has_earned_from_channel(channel_id, user_id):
    with _lock:
        cur = _conn.cursor()
        cur.execute(
            "SELECT 1 FROM channel_earnings WHERE channel_id=? AND user_id=?",
            (channel_id, user_id),
        )
        return cur.fetchone() is not None


def record_channel_earning(channel_id, user_id):
    with _lock:
        cur = _conn.cursor()
        try:
            cur.execute(
                "INSERT INTO channel_earnings (user_id, channel_id, earned_at) VALUES (?, ?, ?)",
                (user_id, channel_id, int(time.time())),
            )
            _conn.commit()
        except sqlite3.IntegrityError:
            pass


def find_earning_campaign(user_id, channel_id_candidates):
    """
    برای یک کاربر که وضعیت عضویتش توی یه کانال (که ممکنه با @یوزرنیم یا
    آیدی عددی شناخته بشه) عوض شده، آخرین کمپینی که ازش کوین گرفته رو پیدا
    میکنه - تا بشه صاحب اون کمپین رو برای جبران/کسر کوین پیدا کرد.
    """
    if not channel_id_candidates:
        return None
    with _lock:
        cur = _conn.cursor()
        placeholders = ",".join("?" for _ in channel_id_candidates)
        cur.execute(
            f"""SELECT campaigns.* FROM campaign_joins
                JOIN campaigns ON campaigns.id = campaign_joins.campaign_id
                WHERE campaign_joins.user_id=? AND campaigns.channel_id IN ({placeholders})
                ORDER BY campaign_joins.joined_at DESC LIMIT 1""",
            (user_id, *channel_id_candidates),
        )
        return cur.fetchone()


def mark_channel_left(user_id, channel_id):
    """اگه این کاربر تازه از کانال خارج شده (و قبلا 'خارج‌شده' ثبت نشده بود)، True برمیگردونه."""
    with _lock:
        cur = _conn.cursor()
        cur.execute(
            "UPDATE channel_earnings SET left_at=? WHERE user_id=? AND channel_id=? AND left_at IS NULL",
            (int(time.time()), user_id, channel_id),
        )
        changed = cur.rowcount > 0
        _conn.commit()
        return changed


def mark_channel_rejoined(user_id, channel_id):
    """اگه این کاربر که قبلا خارج شده بود دوباره برگشته، True برمیگردونه."""
    with _lock:
        cur = _conn.cursor()
        cur.execute(
            "UPDATE channel_earnings SET left_at=NULL WHERE user_id=? AND channel_id=? AND left_at IS NOT NULL",
            (user_id, channel_id),
        )
        changed = cur.rowcount > 0
        _conn.commit()
        return changed


def force_deduct_coins(user_id, amount):
    """کسر اجباری کوین (برای اصلاحات خودکار سیستم) - هیچ‌وقت منفی نمیشه."""
    with _lock:
        cur = _conn.cursor()
        cur.execute(
            "UPDATE users SET coins = MAX(coins - ?, 0) WHERE user_id=?", (amount, user_id)
        )
        _conn.commit()


def stop_campaign(campaign_id):
    """
    کمپین رو متوقف میکنه و کوین‌های باقی‌مونده (مصرف‌نشده) رو به صاحبش برمیگردونه.
    مقدار کوین بازگشتی رو برمیگردونه.
    """
    with _lock:
        cur = _conn.cursor()
        cur.execute("SELECT owner_id, remaining, status FROM campaigns WHERE id=?", (campaign_id,))
        row = cur.fetchone()
        if not row or row["status"] != "active":
            return 0
        refund = row["remaining"]
        cur.execute("UPDATE campaigns SET status='stopped', remaining=0 WHERE id=?", (campaign_id,))
        if refund:
            cur.execute(
                "UPDATE users SET coins = coins + ? WHERE user_id=?", (refund, row["owner_id"])
            )
        _conn.commit()
        return refund


def list_campaigns_by_owner(owner_id):
    with _lock:
        cur = _conn.cursor()
        cur.execute(
            "SELECT * FROM campaigns WHERE owner_id=? ORDER BY created_at DESC",
            (owner_id,),
        )
        return cur.fetchall()


# ---------------------------------------------------------------------------
# پرداخت‌ها (کارت به کارت)
# ---------------------------------------------------------------------------
def create_payment(user_id, coins, amount_toman, receipt_file):
    with _lock:
        cur = _conn.cursor()
        cur.execute(
            """INSERT INTO payments
               (user_id, coins, amount_toman, receipt_file, status, created_at)
               VALUES (?, ?, ?, ?, 'pending', ?)""",
            (user_id, coins, amount_toman, receipt_file, int(time.time())),
        )
        _conn.commit()
        return cur.lastrowid


def get_payment(payment_id):
    with _lock:
        cur = _conn.cursor()
        cur.execute("SELECT * FROM payments WHERE id=?", (payment_id,))
        return cur.fetchone()


def set_payment_status(payment_id, status, reviewed_by):
    with _lock:
        cur = _conn.cursor()
        cur.execute(
            """UPDATE payments SET status=?, reviewed_by=?, reviewed_at=?
               WHERE id=?""",
            (status, reviewed_by, int(time.time()), payment_id),
        )
        _conn.commit()


def try_set_payment_status(payment_id, new_status, reviewed_by):
    """
    مثل set_payment_status ولی فقط زمانی وضعیت رو عوض میکنه که پرداخت
    هنوز 'pending' باشه (به‌صورت اتمیک، توی یک کوئری). این جلوی این رو
    میگیره که یه دابل‌کلیک سریع یا دو تا ادمین هم‌زمان، یه پرداخت رو
    دوبار تایید کنن و کوین دوبار به حساب کاربر اضافه بشه.
    برمیگردونه: True فقط اگه همین فراخوانی واقعاً وضعیت رو عوض کرده باشه.
    """
    with _lock:
        cur = _conn.cursor()
        cur.execute(
            """UPDATE payments SET status=?, reviewed_by=?, reviewed_at=?
               WHERE id=? AND status='pending'""",
            (new_status, reviewed_by, int(time.time()), payment_id),
        )
        _conn.commit()
        return cur.rowcount > 0


def list_pending_payments():
    with _lock:
        cur = _conn.cursor()
        cur.execute("SELECT * FROM payments WHERE status='pending' ORDER BY id ASC")
        return cur.fetchall()


# ---------------------------------------------------------------------------
# پشتیبانی (تیکت)
# ---------------------------------------------------------------------------
def create_ticket(user_id, message):
    with _lock:
        cur = _conn.cursor()
        cur.execute(
            "INSERT INTO tickets (user_id, message, status, created_at) VALUES (?, ?, 'open', ?)",
            (user_id, message, int(time.time())),
        )
        _conn.commit()
        return cur.lastrowid


def list_open_tickets():
    with _lock:
        cur = _conn.cursor()
        cur.execute("SELECT * FROM tickets WHERE status='open' ORDER BY id ASC")
        return cur.fetchall()


def close_ticket(ticket_id):
    with _lock:
        cur = _conn.cursor()
        cur.execute("UPDATE tickets SET status='answered' WHERE id=?", (ticket_id,))
        _conn.commit()


def get_ticket(ticket_id):
    with _lock:
        cur = _conn.cursor()
        cur.execute("SELECT * FROM tickets WHERE id=?", (ticket_id,))
        return cur.fetchone()


# ---------------------------------------------------------------------------
# دکمه‌های منو (کاملا قابل مدیریت توسط ادمین)
# ---------------------------------------------------------------------------
def list_menu_buttons():
    with _lock:
        cur = _conn.cursor()
        cur.execute("SELECT * FROM menu_buttons ORDER BY position ASC")
        return cur.fetchall()


def list_enabled_menu_buttons():
    with _lock:
        cur = _conn.cursor()
        cur.execute("SELECT * FROM menu_buttons WHERE enabled=1 ORDER BY position ASC")
        return cur.fetchall()


def get_menu_button(button_key):
    with _lock:
        cur = _conn.cursor()
        cur.execute("SELECT * FROM menu_buttons WHERE button_key=?", (button_key,))
        return cur.fetchone()


def get_menu_button_by_id(button_id):
    with _lock:
        cur = _conn.cursor()
        cur.execute("SELECT * FROM menu_buttons WHERE id=?", (button_id,))
        return cur.fetchone()


def get_menu_button_by_label(label):
    with _lock:
        cur = _conn.cursor()
        cur.execute("SELECT * FROM menu_buttons WHERE label=? AND enabled=1", (label,))
        return cur.fetchone()


def label_taken(label, exclude_id=None):
    with _lock:
        cur = _conn.cursor()
        if exclude_id is not None:
            cur.execute(
                "SELECT 1 FROM menu_buttons WHERE label=? AND id != ?", (label, exclude_id)
            )
        else:
            cur.execute("SELECT 1 FROM menu_buttons WHERE label=?", (label,))
        return cur.fetchone() is not None


def rename_menu_button(button_id, new_label):
    with _lock:
        cur = _conn.cursor()
        cur.execute("UPDATE menu_buttons SET label=? WHERE id=?", (new_label, button_id))
        _conn.commit()


def set_menu_button_enabled(button_id, enabled):
    with _lock:
        cur = _conn.cursor()
        cur.execute("UPDATE menu_buttons SET enabled=? WHERE id=?", (int(enabled), button_id))
        _conn.commit()


def add_custom_button(label, text):
    with _lock:
        cur = _conn.cursor()
        cur.execute("SELECT COALESCE(MAX(position), 0) + 1 p FROM menu_buttons")
        pos = cur.fetchone()["p"]
        key = f"custom_{int(time.time() * 1000)}"
        cur.execute(
            """INSERT INTO menu_buttons
               (button_key, label, enabled, position, kind, custom_text)
               VALUES (?, ?, 1, ?, 'custom', ?)""",
            (key, label, pos, text),
        )
        _conn.commit()
        return cur.lastrowid


def update_custom_button_text(button_id, text):
    with _lock:
        cur = _conn.cursor()
        cur.execute(
            "UPDATE menu_buttons SET custom_text=? WHERE id=? AND kind='custom'",
            (text, button_id),
        )
        _conn.commit()


def delete_menu_button(button_id):
    """فقط دکمه‌های سفارشی قابل حذفن؛ دکمه‌های اصلی فقط قابل غیرفعال‌سازی هستن."""
    with _lock:
        cur = _conn.cursor()
        cur.execute("DELETE FROM menu_buttons WHERE id=? AND kind='custom'", (button_id,))
        _conn.commit()


def move_menu_button(button_id, direction):
    """direction: -1 برای بالا، +1 برای پایین. جای دو دکمه‌ی همسایه رو عوض میکنه."""
    with _lock:
        cur = _conn.cursor()
        cur.execute("SELECT * FROM menu_buttons ORDER BY position ASC")
        rows = cur.fetchall()
        idx = next((i for i, r in enumerate(rows) if r["id"] == button_id), None)
        if idx is None:
            return
        swap_idx = idx + direction
        if swap_idx < 0 or swap_idx >= len(rows):
            return
        a, b = rows[idx], rows[swap_idx]
        cur.execute("UPDATE menu_buttons SET position=? WHERE id=?", (b["position"], a["id"]))
        cur.execute("UPDATE menu_buttons SET position=? WHERE id=?", (a["position"], b["id"]))
        _conn.commit()


# ---------------------------------------------------------------------------
# متن‌های قابل‌ویرایش ربات (رجیستری کامل توی texts.py هست)
# ---------------------------------------------------------------------------
def get_text(key):
    with _lock:
        cur = _conn.cursor()
        cur.execute("SELECT value FROM texts WHERE key=?", (key,))
        row = cur.fetchone()
    if row:
        return row["value"]
    entry = texts_registry.TEXT_DEFAULTS.get(key)
    return entry[2] if entry else ""


def set_text(key, value):
    with _lock:
        cur = _conn.cursor()
        cur.execute(
            "INSERT INTO texts (key, value) VALUES (?, ?) "
            "ON CONFLICT(key) DO UPDATE SET value=excluded.value",
            (key, value),
        )
        _conn.commit()


def is_text_customized(key):
    with _lock:
        cur = _conn.cursor()
        cur.execute("SELECT 1 FROM texts WHERE key=?", (key,))
        return cur.fetchone() is not None


# ---------------------------------------------------------------------------
# آمار
# ---------------------------------------------------------------------------
def get_stats():
    with _lock:
        cur = _conn.cursor()
        stats = {}
        cur.execute("SELECT COUNT(*) c FROM users")
        stats["users"] = cur.fetchone()["c"]
        cur.execute("SELECT COALESCE(SUM(coins),0) s FROM users")
        stats["total_coins"] = cur.fetchone()["s"]
        cur.execute("SELECT COUNT(*) c FROM campaigns WHERE status='active'")
        stats["active_campaigns"] = cur.fetchone()["c"]
        cur.execute("SELECT COUNT(*) c FROM campaigns")
        stats["total_campaigns"] = cur.fetchone()["c"]
        cur.execute(
            "SELECT COALESCE(SUM(amount_toman),0) s FROM payments WHERE status='approved'"
        )
        stats["total_income"] = cur.fetchone()["s"]
        cur.execute("SELECT COUNT(*) c FROM payments WHERE status='pending'")
        stats["pending_payments"] = cur.fetchone()["c"]
        cur.execute("SELECT COUNT(*) c FROM campaign_joins")
        stats["total_joins"] = cur.fetchone()["c"]
        cur.execute("SELECT COUNT(*) c FROM tickets WHERE status='open'")
        stats["open_tickets"] = cur.fetchone()["c"]
        return stats
