import asyncio
import json
import os
import re
import time
import random
import logging
import contextlib
import telethon
from collections import defaultdict

logging.getLogger().setLevel(logging.CRITICAL)
from statistics import mean
from urllib.parse import unquote, quote_plus
from types import SimpleNamespace

from telethon import TelegramClient, events, Button
from telethon.errors import FloodWaitError
from telethon import functions, types
from telethon.tl.functions.payments import GetStarGiftsRequest, GetResaleStarGiftsRequest
from telethon.errors import RPCError

try:
    from curl_cffi import requests as http_requests
    CURL_CFFI = True
except Exception:
    import requests as http_requests
    CURL_CFFI = False


# مقادیر اختصاصی API را وارد کنید
API_ID = 12345678  # API ID عددی خود را جایگزین کنید
API_HASH = "YOUR_API_HASH"  # API Hash خود را جایگزین کنید

SESSION_NAME = "gift_market_userbot"
ADMIN_ID = 8381464796  # آیدی عددی ادمین
ADMIN_USERNAME = "Morfin_Admin"  # آیدی ادمین

MRKT_BOT = "@mrkt"
MRKT_APP_SHORT_NAME = "app"
MRKT_AUTH_URL = "https://api.tgmrkt.io/api/v1/auth"
MRKT_GIFTS_URL = "https://api.tgmrkt.io/api/v1/gifts/saling"
PORTALS_BOT = "@portals"
PORTALS_APP_SHORT_NAME = "market"
PORTALS_GIFTS_URL = "https://portal-market.com/api/nfts/search"
MRKT_MINI_APP_URL = "https://t.me/mrkt/app"
PORTALS_MINI_APP_URL = "https://t.me/portals/market?startapp="

DEFAULT_MARKET = "both"

RANDOM_INTERVAL = 120
SET_INTERVAL = 180
POST_INTERVAL = SET_INTERVAL
MARKET_LIMIT = 20
MAX_MARKET_PAGES = 5
SET_SIZE = 3
STAR_MARKET_INTERVAL = 300
STAR_FLOOR_COUNT = 3
STAR_MIN_DISCOUNT_PERCENT = 4.0
STAR_MAX_GIFT_TYPES = 1000
STAR_RESALE_LIMIT = 10
STAR_MAX_PRICE = 550
STAR_RATE_BOT = "@HashTrxChecker2Bot"
STAR_RATE_COMMAND = "استارز"
STAR_RATE_INTERVAL = 300

CONFIG_FILE = "gift_market_config.json"

DEFAULT_CONFIG = {
    "channel_id": "100446577678",
    "channel_title": "NFT Finder | 礼物搜寻器",
    "channel_username": "NftFinderx",  # آیدی کانال ارسال گزارشات

    "enabled": True,
    "interval_seconds": SET_INTERVAL,

    "background_mode": "all",
    "background_name": None,

    "market": DEFAULT_MARKET,

    "finder_mode": "set",
    "underfloor_percent": 10.0,

    "last_set_signature": None,
    "last_finder_signature": None,
    "posted_gift_keys": [],
    "posted_backgrounds": [],
    "star_last_alerts": {},
    "star_usd_rate": None,
    "star_rate_updated": 0,

    "admin_id": ADMIN_ID,
}

def load_config():
    if not os.path.exists(CONFIG_FILE):
        data = json.loads(json.dumps(DEFAULT_CONFIG))
        save_config(data)
        return data

    try:
        with open(CONFIG_FILE, "r", encoding="utf-8") as f:
            data = json.load(f)

        for key, value in DEFAULT_CONFIG.items():
            if key not in data:
                data[key] = value

        data["enabled"] = True
        data["finder_mode"] = "automatic"
        data["background_mode"] = "all"
        data["background_name"] = None
        data["market"] = DEFAULT_MARKET
        data["interval_seconds"] = SET_INTERVAL
        save_config(data)

        return data

    except Exception as e:
        print("Config load error:", repr(e))
        return json.loads(json.dumps(DEFAULT_CONFIG))


def save_config(data):
    try:
        tmp = CONFIG_FILE + ".tmp"

        with open(tmp, "w", encoding="utf-8") as f:
            json.dump(
                data,
                f,
                ensure_ascii=False,
                indent=2
            )

        os.replace(tmp, CONFIG_FILE)

    except Exception as e:
        print("Config save error:", repr(e))

_NULL = open(os.devnull, "w", encoding="utf-8")

config = load_config()

client = TelegramClient(
    SESSION_NAME,
    API_ID,
    API_HASH
)

admin_states = {}

market_tokens = {
    "mrkt": {
        "token": None,
        "updated": 0
    },
    "portals": {
        "token": None,
        "updated": 0
    }
}

market_snapshot = {"gifts": [], "updated": 0.0}
market_snapshot_lock = asyncio.Lock()


def is_admin(event):
    sender_id = getattr(event, "sender_id", None)
    current_admin = config.get("admin_id") or ADMIN_ID

    return (
        sender_id is not None
        and current_admin is not None
        and int(sender_id) == int(current_admin)
    )


def clean_username(value):
    value = (value or "").strip()
    return value.lstrip("@").strip()


def normalize_text(value):
    if value is None:
        return ""
    return str(value).strip().casefold()


def first_value(data, *keys, default=None):
    if not isinstance(data, dict):
        return default

    for key in keys:
        value = data.get(key)
        if value is not None and value != "":
            return value

    return default


def parse_price(value):
    if value is None:
        return None

    if isinstance(value, bool):
        return None

    try:
        price = float(value)
    except Exception:
        text = str(value).replace(",", "").strip()
        try:
            price = float(text)
        except Exception:
            return None

    if abs(price) >= 10_000_000:
        price /= 1_000_000_000

    return price


def format_ton(value):
    price = parse_price(value)

    if price is None:
        return "N/A"

    if abs(price) < 0.000000001:
        price = 0.0

    return f"{price:,.4f}".rstrip("0").rstrip(".") + " TON"


def safe_html(text):
    if text is None:
        return ""

    return (
        str(text)
        .replace("&", "&amp;")
        .replace("<", "&lt;")
        .replace(">", "&gt;")
    )


def market_label():
    market = config.get("market", DEFAULT_MARKET)

    if market == "mrkt":
        return "MRKT"

    if market == "portals":
        return "Portals"

    return "MRKT + Portals"


def background_label():
    if config.get("background_mode") == "one":
        return config.get("background_name") or "تعیین نشده"

    return "همه بک‌گراندها"


def admin_panel_text():
    channel = config.get("channel_title")
    username = config.get("channel_username")

    if channel:
        channel_text = safe_html(channel)
        if username:
            channel_text += f" (@{safe_html(username)})"
    else:
        channel_text = "تنظیم نشده"

    return (
        "🎁 <b>Gift Market</b>\n\n"
        f"📢 <b>کانال:</b> {channel_text}\n\n"
        "برای تغییر کانال: <code>/channel @Channel</code>"
    )


def admin_panel_buttons():
    return []

def background_buttons():
    return [
        [
            Button.inline("🌐 همه بک‌گراندها", b"bg:all")
        ],
        [
            Button.inline("🎨 یک بک‌گراند مشخص", b"bg:one")
        ],
        [
            Button.inline("🔙 بازگشت", b"admin:back")
        ]
    ]


def market_buttons():
    return [
        [
            Button.inline("🔷 MRKT", b"market:mrkt"),
            Button.inline("💎 Portals", b"market:portals")
        ],
        [
            Button.inline("🔷 + 💎 هر دو", b"market:both")
        ],
        [
            Button.inline("🔙 بازگشت", b"admin:back")
        ]
    ]

def _http_get(url, headers=None, params=None):
    kwargs = {
        "headers": headers or {},
        "params": params or {},
        "timeout": 30,
    }

    if CURL_CFFI:
        kwargs["impersonate"] = "chrome110"

    response = http_requests.get(url, **kwargs)

    if response.status_code >= 400:
        raise RuntimeError(
            f"HTTP {response.status_code}: "
            f"{response.text[:500]}"
        )

    return response.json()


def _http_post(url, headers=None, json_data=None):
    kwargs = {
        "headers": headers or {},
        "json": json_data or {},
        "timeout": 30,
    }

    if CURL_CFFI:
        kwargs["impersonate"] = "chrome110"

    response = http_requests.post(url, **kwargs)

    if response.status_code >= 400:
        raise RuntimeError(
            f"HTTP {response.status_code}: "
            f"{response.text[:500]}"
        )

    return response.json()


async def http_get(url, headers=None, params=None):
    return await asyncio.to_thread(
        _http_get,
        url,
        headers,
        params
    )


async def http_post(url, headers=None, json_data=None):
    return await asyncio.to_thread(
        _http_post,
        url,
        headers,
        json_data
    )


async def get_miniapp_init_data(
    bot_username,
    app_short_name,
    platform="android"
):
    entity = await client.get_entity(bot_username)
    peer = await client.get_input_entity(entity)

    RequestAppWebView = getattr(
        functions.messages,
        "RequestAppWebViewRequest",
        None
    )

    if RequestAppWebView is not None:
        bot = types.InputUser(
            user_id=entity.id,
            access_hash=entity.access_hash
        )

        bot_app = types.InputBotAppShortName(
            bot_id=bot,
            short_name=app_short_name
        )

        web_view = await client(
            RequestAppWebView(
                peer=peer,
                app=bot_app,
                platform=platform
            )
        )
    else:
        RequestWebView = getattr(
            functions.messages,
            "RequestWebViewRequest",
            None
        )

        if RequestWebView is None:
            raise RuntimeError(
                "این نسخه Telethon هیچ‌کدام از RequestAppWebViewRequest "
                "یا RequestWebViewRequest را ندارد.\n"
                "Telethon را با دستور زیر به‌روز کن: pip install -U telethon"
            )

        app_url = (
            MRKT_MINI_APP_URL
            if bot_username.lower() == MRKT_BOT.lower() and app_short_name == MRKT_APP_SHORT_NAME
            else PORTALS_MINI_APP_URL
            if bot_username.lower() == PORTALS_BOT.lower() and app_short_name == PORTALS_APP_SHORT_NAME
            else f"https://t.me/{bot_username.lstrip('@')}/{app_short_name}"
        )

        web_view = await client(
            RequestWebView(
                peer=peer,
                bot=peer,
                platform=platform,
                url=app_url
            )
        )

    url = getattr(web_view, "url", None)

    if not url or "tgWebAppData=" not in url:
        raise RuntimeError(
            f"Mini App did not return tgWebAppData for {bot_username}.\n"
            f"Returned URL: {url or 'EMPTY'}"
        )

    encoded = url.split("tgWebAppData=", 1)[1]
    encoded = encoded.split("&tgWebAppVersion", 1)[0]

    init_data = unquote(encoded)

    if not init_data:
        raise RuntimeError(
            f"Empty Mini App init data for {bot_username}"
        )

    return init_data


async def get_mrkt_token(force=False):
    cached = market_tokens["mrkt"]

    if (
        not force
        and cached["token"]
        and time.time() - cached["updated"] < 18 * 60 * 60
    ):
        return cached["token"]

    init_data = await get_miniapp_init_data(
        MRKT_BOT,
        MRKT_APP_SHORT_NAME,
        "android"
    )

    result = await http_post(
        MRKT_AUTH_URL,
        json_data={"data": init_data}
    )

    token = first_value(
        result,
        "token",
        "access_token"
    )

    if not token:
        raise RuntimeError(
            "MRKT auth response did not contain token"
        )

    cached["token"] = token
    cached["updated"] = time.time()

    print("MRKT Mini App authentication refreshed.")

    return token


async def get_portals_auth(force=False):
    cached = market_tokens["portals"]

    if (
        not force
        and cached["token"]
        and time.time() - cached["updated"] < 18 * 60 * 60
    ):
        return cached["token"]

    init_data = await get_miniapp_init_data(
        PORTALS_BOT,
        PORTALS_APP_SHORT_NAME,
        "android"
    )

    auth_data = f"tma {init_data}"

    cached["token"] = auth_data
    cached["updated"] = time.time()

    print("Portals Mini App authentication refreshed.")

    return auth_data


def telegram_gift_slug(name, number=None, existing_slug=None):
    if existing_slug:
        slug = str(existing_slug).strip().strip("/")
        if slug.startswith("https://t.me/nft/"):
            return slug.rsplit("/", 1)[-1]
        if slug.startswith("t.me/nft/"):
            return slug.rsplit("/", 1)[-1]
        if re.search(r"-\d+$", slug):
            return slug

    text = str(name or "").strip()
    if not text or number in (None, "", "-"):
        return None

    parts = re.findall(r"[A-Za-z0-9]+", text)
    if not parts:
        return None

    base = "".join(part[:1].upper() + part[1:] for part in parts)
    return f"{base}-{str(number).strip()}"


def telegram_gift_link(name, number=None, existing_slug=None):
    slug = telegram_gift_slug(name, number, existing_slug)
    return f"https://t.me/nft/{slug}" if slug else None


def normalize_mrkt_gift(raw):
    if not isinstance(raw, dict):
        return None

    price = first_value(
        raw,
        "price",
        "salePrice",
        "resalePrice",
        "amount"
    )

    price = parse_price(price)

    if price is None or price <= 0:
        return None

    name = first_value(
        raw,
        "collectionName",
        "giftName",
        "name",
        "title",
        default="Unknown Gift"
    )

    model = first_value(raw, "modelName", "model", default="-")
    backdrop = first_value(raw, "backdropName", "backdrop", "backgroundName", "background", default="-")
    symbol = first_value(raw, "symbolName", "symbol", default="-")
    number = first_value(raw, "number", "num", "giftNumber")
    gift_id = first_value(raw, "id", "giftId", "nftId", "slug", default="")
    link = first_value(raw, "link", "url", "giftUrl", "nftUrl")
    slug = first_value(raw, "slug")

    official_link = telegram_gift_link(name, number, slug)
    if official_link:
        link = official_link

    image = first_value(raw, "image", "imageUrl", "photo", "photoUrl", "previewUrl")

    return {
        "market": "MRKT",
        "id": str(gift_id),
        "name": str(name),
        "model": str(model),
        "backdrop": str(backdrop),
        "symbol": str(symbol),
        "number": str(number) if number is not None else "",
        "price": price,
        "link": link,
        "image": image,
        "raw": raw
    }


async def fetch_mrkt_gifts(backdrop=None):
    token = await get_mrkt_token()

    headers = {
        "Authorization": token,
        "Cookie": f"access_token={token}",
        "Origin": "https://cdn.tgmrkt.io",
        "Referer": "https://cdn.tgmrkt.io/",
        "Accept": "application/json, text/plain, */*",
        "Accept-Language": "en-GB,en-US;q=0.9,en;q=0.8",
        "Cache-Control": "no-cache",
        "Pragma": "no-cache",
        "User-Agent": "Mozilla/5.0"
    }

    cursor = ""
    seen_cursors = set()
    seen_ids = set()
    normalized = []
    page = 0

    while page < MAX_MARKET_PAGES:
        payload = {
            "collectionNames": [],
            "modelNames": [],
            "backdropNames": [backdrop] if backdrop else [],
            "symbolNames": [],
            "ordering": "Price",
            "lowToHigh": True,
            "maxPrice": None,
            "minPrice": None,
            "mintable": None,
            "number": None,
            "count": MARKET_LIMIT,
            "cursor": cursor,
            "query": None,
            "promotedFirst": False,
        }

        try:
            result = await http_post(
                MRKT_GIFTS_URL,
                headers=headers,
                json_data=payload
            )
        except RuntimeError as e:
            message = str(e)
            if "401" in message or "403" in message:
                token = await get_mrkt_token(force=True)
                headers["Authorization"] = token
                headers["Cookie"] = f"access_token={token}"
                result = await http_post(
                    MRKT_GIFTS_URL,
                    headers=headers,
                    json_data=payload
                )
            elif "429" in message:
                break
            else:
                raise

        gifts = first_value(result, "gifts", "results", "items", "data", default=[])
        if isinstance(gifts, dict):
            gifts = first_value(gifts, "gifts", "results", "items", default=[])
        if not isinstance(gifts, list):
            gifts = []

        new_count = 0
        for item in gifts:
            gift = normalize_mrkt_gift(item)
            if not gift:
                continue
            key = gift_signature(gift) if "gift_signature" in globals() else f"{gift.get('market')}:{gift.get('id')}"
            if key in seen_ids:
                continue
            seen_ids.add(key)
            normalized.append(gift)
            new_count += 1

        next_cursor = first_value(
            result,
            "cursor",
            "nextCursor",
            "next_cursor",
            "next",
            default=None
        )
        next_cursor = str(next_cursor or "").strip()

        page += 1

        if not gifts or len(gifts) < MARKET_LIMIT or not next_cursor or next_cursor in seen_cursors or new_count == 0:
            break

        seen_cursors.add(next_cursor)
        cursor = next_cursor

    return normalized


def portals_cap(text):
    text = str(text or "")
    words = re.findall(r"\w+(?:'\w+)?", text)

    for word in words:
        if word:
            text = text.replace(
                word,
                word[0].upper() + word[1:],
                1
            )

    return text


def normalize_portals_gift(raw):
    if not isinstance(raw, dict):
        return None

    price = parse_price(
        first_value(
            raw,
            "price",
            "sale_price"
        )
    )

    if price is None or price <= 0:
        return None

    name = first_value(
        raw,
        "name",
        "gift_name",
        "collection_name",
        default="Unknown Gift"
    )

    attributes = raw.get("attributes") or []

    model = "-"
    symbol = "-"
    backdrop = "-"

    if isinstance(attributes, list):
        for attr in attributes:
            if not isinstance(attr, dict):
                continue

            attr_type = normalize_text(attr.get("type"))
            value = attr.get("value")

            if attr_type == "model":
                model = value or "-"
            elif attr_type == "symbol":
                symbol = value or "-"
            elif attr_type in ("backdrop", "background"):
                backdrop = value or "-"

    model = first_value(raw, "model", "model_name", default=model)
    symbol = first_value(raw, "symbol", "symbol_name", default=symbol)
    backdrop = first_value(raw, "backdrop", "backdrop_name", "background", "background_name", default=backdrop)
    number = first_value(raw, "external_collection_number", "number", "num", "tg_id")
    gift_id = first_value(raw, "id", "nft_id", default="")
    link = first_value(raw, "link", "url", "nft_url")

    official_link = telegram_gift_link(
        name,
        number,
        first_value(raw, "slug", "gift_slug", "nft_slug")
    )
    if official_link:
        link = official_link

    image = first_value(raw, "photo_url", "image_url", "image", "preview_url")

    return {
        "market": "Portals",
        "id": str(gift_id),
        "name": str(name),
        "model": str(model),
        "backdrop": str(backdrop),
        "symbol": str(symbol),
        "number": str(number) if number is not None else "",
        "price": price,
        "link": link,
        "image": image,
        "raw": raw
    }


async def fetch_portals_gifts(backdrop=None):
    auth_data = await get_portals_auth()

    headers = {
        "Authorization": auth_data,
        "Accept": "application/json, text/plain, */*",
        "Origin": "https://portal-market.com",
        "Referer": "https://portal-market.com/",
        "User-Agent": "Mozilla/5.0"
    }

    pages = MAX_MARKET_PAGES
    normalized = []
    seen_listing_keys = set()
    page = 0

    while page < pages:
        params = {
            "offset": page * MARKET_LIMIT,
            "limit": MARKET_LIMIT,
            "sort_by": "price asc",
            "status": "listed"
        }

        if backdrop:
            params["filter_by_backdrops"] = portals_cap(backdrop)

        try:
            result = await http_get(
                PORTALS_GIFTS_URL,
                headers=headers,
                params=params
            )
        except RuntimeError as e:
            message = str(e)

            if "401" in message or "403" in message:
                auth_data = await get_portals_auth(force=True)
                headers["Authorization"] = auth_data

                result = await http_get(
                    PORTALS_GIFTS_URL,
                    headers=headers,
                    params=params
                )
            elif "429" in message:
                break
            else:
                raise

        gifts = first_value(result, "results", "nfts", "items", "data", default=[])

        if isinstance(gifts, dict):
            gifts = first_value(gifts, "results", "nfts", "items", default=[])

        if not isinstance(gifts, list):
            gifts = []

        for item in gifts:
            gift = normalize_portals_gift(item)
            if gift:
                normalized.append(gift)

        new_count = 0
        for item in gifts:
            if not isinstance(item, dict):
                continue
            key = str(first_value(item, "id", "nft_id", "slug", "external_collection_number", default=""))
            if key and key not in seen_listing_keys:
                seen_listing_keys.add(key)
                new_count += 1

        if not gifts or len(gifts) < MARKET_LIMIT or new_count == 0:
            break

        page += 1

    return normalized


async def fetch_market_gifts(backdrop=None):
    now = time.time()
    async with market_snapshot_lock:
        if not backdrop and market_snapshot["gifts"] and now - market_snapshot["updated"] < 50:
            return list(market_snapshot["gifts"])

        market = config.get("market", DEFAULT_MARKET)
        all_gifts = []

        if market in ("mrkt", "both"):
            try:
                all_gifts.extend(await fetch_mrkt_gifts(backdrop))
            except Exception:
                pass

        if market in ("portals", "both"):
            try:
                all_gifts.extend(await fetch_portals_gifts(backdrop))
            except Exception:
                pass

        unique = {}
        for gift in all_gifts:
            key = posted_gift_key(gift)
            if key not in unique:
                unique[key] = gift

        result = list(unique.values())

        floor_by_background = defaultdict(list)
        for item in result:
            bg = get_background(item) if "get_background" in globals() else item.get("backdrop")
            price = parse_price(item.get("price"))
            if bg and price is not None and price > 0:
                floor_by_background[normalize_text(bg)].append(price)

        for item in result:
            bg = get_background(item) if "get_background" in globals() else item.get("backdrop")
            prices = floor_by_background.get(normalize_text(bg), []) if bg else []
            item["floor"] = min(prices) if prices else item.get("price")

        if not backdrop:
            market_snapshot["gifts"] = result
            market_snapshot["updated"] = time.time()
        return result


def get_background(gift):
    value = gift.get("backdrop")

    if value in (None, "", "-"):
        return None

    return str(value).strip()


def select_three_gift_set(gifts, requested_background=None):
    if not gifts:
        return None

    groups = defaultdict(list)
    backdrop_display = {}
    requested_normalized = (
        normalize_text(requested_background)
        if requested_background else None
    )
    posted = set(config.get("posted_gift_keys") or [])

    for gift in gifts:
        backdrop = get_background(gift)
        price = parse_price(gift.get("price"))
        if not backdrop or normalize_text(backdrop) in ("", "-"):
            continue
        if price is None or price <= 0:
            continue

        backdrop_key = normalize_text(backdrop)
        if requested_normalized and backdrop_key != requested_normalized:
            continue

        groups[backdrop_key].append(gift)
        backdrop_display.setdefault(backdrop_key, backdrop)

    candidates = []

    for backdrop_key, items in groups.items():
        items = sorted(items, key=lambda x: parse_price(x.get("price")))
        if len(items) < SET_SIZE:
            continue

        fresh = [
            item for item in items
            if posted_gift_key(item) not in posted
        ]

        if len(fresh) >= SET_SIZE:
            selected = fresh[:SET_SIZE]
            fresh_count = SET_SIZE
        else:
            selected = items[:SET_SIZE]
            fresh_count = sum(
                1 for item in selected
                if posted_gift_key(item) not in posted
            )

        total = sum(parse_price(item.get("price")) for item in selected)
        candidates.append({
            "total": total,
            "backdrop": backdrop_display.get(backdrop_key, backdrop_key),
            "gifts": selected,
            "fresh_count": fresh_count,
        })

    if not candidates:
        return None

    candidates.sort(
        key=lambda item: (
            -item["fresh_count"],
            item["total"],
            normalize_text(item["backdrop"]),
        )
    )

    best = candidates[0]
    return {
        "backdrop": best["backdrop"],
        "gifts": best["gifts"],
        "total": best["total"],
    }


def gift_display_name(gift):
    name = gift.get("name") or "Unknown Gift"
    number = gift.get("number")

    if number:
        return f"{name} #{number}"

    return name


def channel_link_html():
    username = clean_username(config.get("channel_username") or "")
    if username:
        return f"https://t.me/{username}"
    return None


def build_set_message(result):
    gifts = result.get("gifts") or []
    backdrop = safe_html(result.get("backdrop") or "-")

    lines = [
        "🎁 <b>Gift Set ×3</b>",
        f"🎨 <b>{backdrop}</b>",
        ""
    ]

    for index, gift in enumerate(gifts[:3], 1):
        display_name = safe_html(gift_display_name(gift))
        link = gift.get("link")
        if link:
            display_name = f'<a href="{safe_html(link)}">{display_name}</a>'

        listed = format_ton(gift.get("price"))
        lines.append(f"{index}. {display_name} · <b>{listed}</b>")

    lines.append("")
    lines.append(f"💎 <b>Total: {format_ton(result.get('total'))}</b>")

    return "\n".join(lines)


def set_signature(result):
    parts = [result.get("backdrop", "")]

    for gift in result.get("gifts", []):
        parts.append(
            "|".join(
                [
                    gift.get("market", ""),
                    gift.get("id", ""),
                    str(gift.get("price", ""))
                ]
            )
        )

    return "::".join(parts)


def build_single_gift_message(gift, heading="🎁 <b>Gift</b>", extra_lines=None):
    display_name = safe_html(gift_display_name(gift))
    link = gift.get("link")

    if link:
        display_name = f'<a href="{safe_html(link)}">{display_name}</a>'

    model = safe_html(gift.get("model") or "-")
    symbol = safe_html(gift.get("symbol") or "-")
    backdrop = safe_html(gift.get("backdrop") or "-")
    listed = format_ton(gift.get("price"))
    floor = format_ton(gift.get("floor", gift.get("price")))

    lines = [
        heading,
        f"🎁 <b>{display_name}</b>",
        f"🧩 {model} · 🔹 {symbol}",
        f"🎨 {backdrop}",
        f"💰 <b>{listed}</b> · Floor: <b>{floor}</b>",
    ]

    if extra_lines:
        lines.extend(str(x) for x in extra_lines if x)

    return "\n".join(lines)


def gift_signature(gift):
    return "|".join([
        str(gift.get("market", "")),
        str(gift.get("id", "")),
        str(gift.get("price", ""))
    ])


def remember_posted_gift(gift):
    posted = list(config.get("posted_gift_keys") or [])
    key = posted_gift_key(gift)
    if key and key not in posted:
        posted.append(key)
    config["posted_gift_keys"] = posted

    bg = normalize_text(get_background(gift))
    if bg:
        backgrounds = list(config.get("posted_backgrounds") or [])
        if bg not in backgrounds:
            backgrounds.append(bg)
        config["posted_backgrounds"] = backgrounds


def posted_gift_key(gift):
    name = normalize_text(gift.get("name"))
    number = str(gift.get("number") or "").strip()
    if name and number:
        return f"telegram:{name}:{number}"
    return f"{gift.get('market','')}:{gift.get('id','')}"


def select_random_floor_gift(gifts, requested_background=None):
    requested = normalize_text(requested_background) if requested_background else None
    posted = set(config.get("posted_gift_keys") or [])
    used_backgrounds = set(config.get("posted_backgrounds") or [])

    by_bg = defaultdict(list)
    for gift in gifts:
        bg = get_background(gift)
        price = parse_price(gift.get("price"))
        if not bg or price is None or price <= 0:
            continue
        if requested and normalize_text(bg) != requested:
            continue
        if posted_gift_key(gift) in posted:
            continue
        by_bg[normalize_text(bg)].append(gift)

    protected_backgrounds = {
        key for key, items in by_bg.items() if len(items) >= SET_SIZE
    }

    for items in by_bg.values():
        items.sort(key=lambda x: parse_price(x.get("price")))

    candidate_groups = [
        (key, items)
        for key, items in by_bg.items()
        if key not in protected_backgrounds
    ]

    if not candidate_groups:
        candidate_groups = list(by_bg.items())

    fresh = [
        items[0] for key, items in candidate_groups
        if key not in used_backgrounds and items
    ]
    pool = fresh or [items[0] for _, items in candidate_groups if items]
    return random.choice(pool) if pool else None


def select_underfloor_gift(gifts, requested_background=None, minimum_percent=10.0):
    if not gifts:
        return None

    requested = normalize_text(requested_background) if requested_background else None
    groups = defaultdict(list)

    for gift in gifts:
        backdrop = get_background(gift)
        if not backdrop:
            continue
        if requested and normalize_text(backdrop) != requested:
            continue

        key = (
            normalize_text(gift.get("name")),
            normalize_text(gift.get("model")),
            normalize_text(gift.get("symbol")),
            normalize_text(backdrop),
        )
        groups[key].append(gift)

    candidates = []

    for items in groups.values():
        valid = [x for x in items if parse_price(x.get("price")) is not None]
        valid.sort(key=lambda x: parse_price(x.get("price")))

        if len(valid) < SET_SIZE:
            continue

        floor = sum(parse_price(x.get("price")) for x in valid[:SET_SIZE]) / SET_SIZE
        cheapest = valid[0]
        price = parse_price(cheapest.get("price"))

        if price is None or floor <= 0:
            continue

        discount = ((floor - price) / floor) * 100

        if discount >= minimum_percent:
            candidates.append((discount, price, floor, cheapest))

    if not candidates:
        return None

    candidates.sort(key=lambda x: (-x[0], x[1]))
    discount, price, floor, gift = candidates[0]

    return {
        "gift": gift,
        "floor": floor,
        "discount": discount,
    }


async def post_finder_result(force=False):
    if not config.get("enabled") and not force:
        print("Automatic posting is disabled.")
        return False

    channel_id = config.get("channel_id") or config.get("channel_username")
    if not channel_id:
        print("Channel is not configured.")
        return False

    mode = config.get("finder_mode", "set")
    requested_background = None

    if config.get("background_mode") == "one":
        requested_background = config.get("background_name")
        if not requested_background:
            print("Single background mode is active but no name is set.")
            return False

    print(
        "Searching market | "
        f"mode={mode} | "
        f"background={requested_background or 'ALL'} | "
        f"market={config.get('market')}"
    )

    gifts = await fetch_market_gifts(requested_background)
    if not gifts:
        print("No gifts returned from selected markets.")
        return False

    if mode == "set":
        result = select_three_gift_set(gifts, requested_background)
        if not result:
            print("No valid 3-gift set found; nothing was posted to the channel.")
            return False

        signature = set_signature(result)
        if not force and signature == config.get("last_set_signature"):
            print("Same set as previous post; skipped.")
            return False

        message = build_set_message(result)

        try:
            await client.send_message(channel_id, message, parse_mode="html", link_preview=True)
            config["last_set_signature"] = signature
            posted = list(config.get("posted_gift_keys") or [])
            for gift in result["gifts"]:
                key = posted_gift_key(gift)
                if key not in posted:
                    posted.append(key)
            config["posted_gift_keys"] = posted[-5000:]
            backgrounds = list(config.get("posted_backgrounds") or [])
            bg_key = normalize_text(result.get("backdrop"))
            if bg_key and bg_key not in backgrounds:
                backgrounds.append(bg_key)
            config["posted_backgrounds"] = backgrounds[-5000:]
            save_config(config)
            print(f"SET POSTED | Background: {result['backdrop']} | Total: {result['total']}")
            return True
        except FloodWaitError as e:
            print("Channel FloodWait:", e.seconds)
            await asyncio.sleep(e.seconds)
            return False
        except Exception as e:
            print("Channel post error:", type(e).__name__, repr(e))
            return False

    if mode == "random":
        gift = select_random_floor_gift(gifts, requested_background)
        if not gift:
            print("No random gift found.")
            return False

        signature = gift_signature(gift)
        if not force and signature == config.get("last_finder_signature"):
            alternatives = [g for g in gifts if gift_signature(g) != signature]
            if alternatives:
                gift = random.choice(alternatives)
                signature = gift_signature(gift)
            else:
                print("Only the previous random gift is available; skipped.")
                return False

        message = build_single_gift_message(gift, "🎲 <b>Random Gift</b>")

    elif mode == "underfloor":
        minimum = float(config.get("underfloor_percent", 10.0) or 10.0)
        result = select_underfloor_gift(gifts, requested_background, minimum)
        if not result:
            print(f"No gift found at least {minimum}% below local floor.")
            return False

        gift = result["gift"]
        signature = gift_signature(gift)
        if not force and signature == config.get("last_finder_signature"):
            print("Same under-floor gift as previous post; skipped.")
            return False

        message = build_single_gift_message(
            gift,
            "📉 <b>Gift Below Floor</b>",
            [
                f"📊 <b>Local Floor:</b> {format_ton(result['floor'])}",
                f"📉 <b>Discount:</b> {result['discount']:.1f}%",
            ]
        )

    else:
        print(f"Unknown finder mode: {mode}")
        return False

    try:
        await client.send_message(channel_id, message, parse_mode="html", link_preview=True)
        config["last_finder_signature"] = signature
        posted = list(config.get("posted_gift_keys") or [])
        key = posted_gift_key(gift)
        if key not in posted:
            posted.append(key)
        config["posted_gift_keys"] = posted[-5000:]
        backgrounds = list(config.get("posted_backgrounds") or [])
        bg_key = normalize_text(get_background(gift))
        if bg_key and bg_key not in backgrounds:
            backgrounds.append(bg_key)
        config["posted_backgrounds"] = backgrounds[-5000:]
        save_config(config)
        print(f"{mode.upper()} POSTED | {gift_display_name(gift)} | {gift.get('price')} TON")
        return True
    except FloodWaitError as e:
        print("Channel FloodWait:", e.seconds)
        await asyncio.sleep(e.seconds)
        return False
    except Exception as e:
        print("Channel post error:", type(e).__name__, repr(e))
        return False


async def post_best_set(force=False):
    return await post_finder_result(force=force)


STAR_DEBUG_FILE = os.path.join(
    os.path.dirname(os.path.abspath(__file__)),
    "stars_market_debug.log"
)


def star_debug(message):
    try:
        with open(STAR_DEBUG_FILE, "a", encoding="utf-8") as f:
            f.write(
                time.strftime("[%Y-%m-%d %H:%M:%S] ")
                + str(message)
                + "\n"
            )
    except Exception:
        pass


def stars_to_usd(stars):
    rate = config.get("star_usd_rate")
    if not rate:
        return None
    try:
        return float(stars) * float(rate)
    except Exception:
        return None


def get_star_gift_price(gift):
    if getattr(gift, "resale_ton_only", False):
        return None

    resell_amount = getattr(gift, "resell_amount", None)
    if resell_amount:
        values = resell_amount if isinstance(resell_amount, (list, tuple)) else [resell_amount]
        for amount in values:
            value = getattr(amount, "amount", None)
            if value is None and isinstance(amount, int):
                value = amount
            try:
                if value is not None and int(value) > 0:
                    return int(value)
            except Exception:
                pass

    resale_amount = getattr(gift, "resale_amount", None)
    if resale_amount:
        values = resale_amount if isinstance(resale_amount, (list, tuple)) else [resale_amount]
        for amount in values:
            value = getattr(amount, "amount", None)
            if value is None and isinstance(amount, int):
                value = amount
            try:
                if value is not None and int(value) > 0:
                    return int(value)
            except Exception:
                pass

    direct = getattr(gift, "resell_stars", None)
    if direct is not None:
        try:
            direct = int(direct)
            if direct > 0:
                return direct
        except Exception:
            pass

    return None


def get_star_gift_title(gift):
    title = getattr(gift, "title", None)
    return str(title) if title else "Unknown Gift"


def get_star_gift_number(gift):
    number = getattr(gift, "num", None)
    return str(number) if number is not None else "?"


def get_star_gift_link(gift):
    slug = getattr(gift, "slug", None)
    if not slug:
        return None

    slug = str(slug).strip().strip("/")

    if slug.startswith("https://t.me/nft/"):
        return slug

    if slug.startswith("t.me/nft/"):
        return "https://" + slug

    return "https://t.me/nft/" + slug


def get_star_gift_key(gift):
    slug = getattr(gift, "slug", None)
    if slug:
        return str(slug).strip().strip("/")

    gift_id = getattr(gift, "id", None)
    number = getattr(gift, "num", None)
    return f"{gift_id}:{number}"


async def update_star_rate():
    try:
        await client.send_message(
            STAR_RATE_BOT,
            STAR_RATE_COMMAND
        )

        await asyncio.sleep(2)

        messages = await client.get_messages(
            STAR_RATE_BOT,
            limit=10
        )

        patterns = [
            r"1(?:\.00)?\s*STARS?\s*=\s*\$([0-9]+(?:\.[0-9]+)?)",
            r"STARS?\s*=\s*\$([0-9]+(?:\.[0-9]+)?)"
        ]

        for message in messages:
            text = message.raw_text or ""

            for pattern in patterns:
                match = re.search(
                    pattern,
                    text,
                    re.IGNORECASE
                )

                if match:
                    rate = float(match.group(1))

                    if rate > 0:
                        config["star_usd_rate"] = rate
                        config["star_rate_updated"] = int(time.time())
                        save_config(config)
                        return True

    except Exception as e:
        star_debug(
            f"RATE ERROR {type(e).__name__}: {e}"
        )

    return False


async def star_rate_loop():
    await asyncio.sleep(5)

    while True:
        try:
            updated = config.get(
                "star_rate_updated",
                0
            ) or 0

            if (
                not config.get("star_usd_rate")
                or time.time() - updated >= STAR_RATE_INTERVAL
            ):
                await update_star_rate()

        except Exception as e:
            star_debug(
                f"RATE LOOP ERROR {type(e).__name__}: {e}"
            )

        await asyncio.sleep(STAR_RATE_INTERVAL)


async def _load_star_catalog_fallback():
    cache_path = "telegram_star_catalog_cache.json"
    cache_max_age = 6 * 60 * 60
    url = "https://raw.githubusercontent.com/Kabachkoov/Telegram-Gift_id/main/README.md"

    try:
        if os.path.exists(cache_path) and time.time() - os.path.getmtime(cache_path) < cache_max_age:
            with open(cache_path, "r", encoding="utf-8") as f:
                data = json.load(f)
            items = data.get("gifts", []) if isinstance(data, dict) else []
            if items:
                star_debug(f"FALLBACK CATALOG CACHE ids={len(items)}")
                return [SimpleNamespace(id=int(x["id"]), title=x.get("title") or "Telegram Gift") for x in items]
    except Exception as e:
        star_debug(f"FALLBACK CACHE READ ERROR {type(e).__name__}: {e}")

    try:
        response = http_requests.get(url, timeout=15, headers={"User-Agent": "Mozilla/5.0"})
        text = getattr(response, "text", "") or ""
        if getattr(response, "status_code", 200) != 200 or not text:
            star_debug(f"FALLBACK CATALOG HTTP ERROR status={getattr(response, 'status_code', None)}")
            return []

        matches = re.findall(r"ID:\s*(\d+)\s*\|\s*Name:\s*([^|\n]+)", text, flags=re.I)
        seen = set()
        gifts = []
        cache_items = []
        for gift_id, title in matches:
            gift_id = int(gift_id)
            if gift_id in seen:
                continue
            seen.add(gift_id)
            title = title.strip()
            if title.lower() == "none":
                title = "Telegram Gift"
            gifts.append(SimpleNamespace(id=gift_id, title=title))
            cache_items.append({"id": gift_id, "title": title})

        if gifts:
            try:
                with open(cache_path, "w", encoding="utf-8") as f:
                    json.dump({"updated": int(time.time()), "gifts": cache_items}, f, ensure_ascii=False)
            except Exception as e:
                star_debug(f"FALLBACK CACHE WRITE ERROR {type(e).__name__}: {e}")

        star_debug(f"FALLBACK CATALOG REMOTE ids={len(gifts)} source=Telegram-Gift_id")
        return gifts
    except Exception as e:
        star_debug(f"FALLBACK CATALOG ERROR {type(e).__name__}: {e}")
        return []


async def get_star_gift_types():
    try:
        star_debug(f"TELETHON VERSION={getattr(telethon, '__version__', 'unknown')}")
    except Exception:
        pass

    try:
        result = await client(GetStarGiftsRequest(hash=0))
        gifts = list(getattr(result, "gifts", []) or [])
        resale_count = sum(1 for gift in gifts if getattr(gift, "availability_resale", None))
        star_debug(
            f"OFFICIAL CATALOG type={type(result).__name__} hash={getattr(result, 'hash', None)} "
            f"catalog={len(gifts)} resale_flag={resale_count}"
        )

        if gifts:
            resale_types = [g for g in gifts if getattr(g, "availability_resale", None)]
            chosen = resale_types or gifts
            star_debug(f"OFFICIAL CATALOG READY total={len(gifts)} resale_types={len(resale_types)}")
            return chosen[:STAR_MAX_GIFT_TYPES]

        try:
            raw = result.stringify()
        except Exception:
            raw = repr(result)
        raw = str(raw).replace("\x00", "")
        if len(raw) > 6000:
            raw = raw[:6000] + " ...[truncated]"
        star_debug(f"OFFICIAL CATALOG EMPTY {raw}")
    except FloodWaitError as e:
        star_debug(f"OFFICIAL CATALOG FLOODWAIT seconds={e.seconds}")
        await asyncio.sleep(e.seconds + 1)
    except Exception as e:
        star_debug(f"OFFICIAL CATALOG ERROR {type(e).__name__}: {e}")

    fallback = await _load_star_catalog_fallback()
    return fallback[:STAR_MAX_GIFT_TYPES]


async def get_star_resale_gifts(gift_id):
    offset = ""
    collected = []

    for page in range(1):
        try:
            request = GetResaleStarGiftsRequest(
                sort_by_price=True,
                sort_by_num=False,
                stars_only=True,
                gift_id=gift_id,
                attributes=None,
                offset=offset,
                limit=STAR_RESALE_LIMIT
            )

            result = await client(request)
            page_gifts = list(getattr(result, "gifts", []) or [])
            collected.extend(page_gifts)

            star_debug(
                f"TELEGRAM RESALE gift_id={gift_id} page={page + 1} "
                f"resale_results={len(page_gifts)} "
                f"result_type={type(result).__name__} "
                f"count={getattr(result, 'count', None)}"
            )

            if page == 0 and page_gifts:
                try:
                    sample = page_gifts[0].stringify()
                except Exception:
                    sample = repr(page_gifts[0])
                sample = str(sample).replace("\x00", "")
                if len(sample) > 5000:
                    sample = sample[:5000] + " ...[truncated]"
                star_debug(f"RESALE SAMPLE {sample}")

            next_offset = getattr(result, "next_offset", None) or ""
            if not page_gifts or not next_offset or next_offset == offset:
                break
            offset = next_offset

        except FloodWaitError as e:
            star_debug(f"RESALE FLOODWAIT gift_id={gift_id} seconds={e.seconds}")
            await asyncio.sleep(e.seconds + 1)
            break

        except Exception as e:
            star_debug(f"RESALE ERROR gift_id={gift_id} {type(e).__name__}: {e}")
            break

    return collected


def get_valid_star_prices(gifts):
    prices = []

    for gift in gifts:
        price = get_star_gift_price(gift)
        if price is not None and price > 0:
            prices.append(price)

    return prices


def calculate_star_floor(gifts):
    prices = get_valid_star_prices(gifts)

    if len(prices) < STAR_FLOOR_COUNT:
        return None

    prices.sort()

    return mean(prices[:STAR_FLOOR_COUNT])


def calculate_star_discount(price, floor):
    if price <= 0 or floor <= 0:
        return 0.0

    return ((floor - price) / floor) * 100


def build_star_market_message(gift, price, floor, discount):
    title = safe_html(get_star_gift_title(gift))
    number = safe_html(get_star_gift_number(gift))
    link = get_star_gift_link(gift)

    gift_name = f"{title} #{number}"
    if link:
        gift_name = f'<a href="{safe_html(link)}">{gift_name}</a>'

    lines = [
        "⭐ <b>Star Gift</b>",
        f"🎁 <b>{gift_name}</b>",
        f"💰 <b>{price:,} Stars</b> · Floor: <b>{floor:,.0f}</b>",
        f"🔥 <b>{discount:.1f}% OFF</b>",
    ]

    usd = stars_to_usd(price)
    if usd is not None:
        lines.append(f"💵 ${usd:,.2f}")

    if link:
        lines.append(f'<a href="{safe_html(link)}">🔗 View NFT</a>')

    return "\n".join(lines)


async def check_star_gift_type(base_gift):
    gift_id = getattr(base_gift, "id", None)

    if not gift_id:
        return None

    gifts = await get_star_resale_gifts(gift_id)
    if not gifts:
        return None

    valid_gifts = []
    for gift in gifts:
        price = get_star_gift_price(gift)
        if price is not None and price > 0:
            valid_gifts.append(gift)

    if len(valid_gifts) < STAR_FLOOR_COUNT:
        return None

    valid_gifts.sort(key=lambda item: get_star_gift_price(item))

    floor_prices = [
        get_star_gift_price(gift)
        for gift in valid_gifts[:STAR_FLOOR_COUNT]
        if get_star_gift_price(gift) is not None
    ]

    if len(floor_prices) < STAR_FLOOR_COUNT:
        return None

    floor = mean(floor_prices)
    cheapest = valid_gifts[0]
    cheapest_price = get_star_gift_price(cheapest)

    if cheapest_price is None or not (0 < cheapest_price < STAR_MAX_PRICE):
        return None

    discount = calculate_star_discount(cheapest_price, floor)

    if discount < STAR_MIN_DISCOUNT_PERCENT:
        return None

    link = get_star_gift_link(cheapest)
    if not link:
        return None

    key = get_star_gift_key(cheapest)
    alerts = config.get("star_last_alerts") or {}
    old_signature = alerts.get(key)

    if isinstance(old_signature, dict) and old_signature.get("price") == cheapest_price:
        return None

    return {
        "gift": cheapest,
        "price": cheapest_price,
        "floor": floor,
        "discount": discount,
        "key": key,
    }


async def post_star_candidate(candidate):
    channel_target = config.get("channel_id") or config.get("channel_username")
    if not candidate or not channel_target:
        return False

    gift = candidate["gift"]
    price = candidate["price"]
    floor = candidate["floor"]
    discount = candidate["discount"]
    key = candidate["key"]

    try:
        await client.send_message(
            channel_target,
            build_star_market_message(gift, price, floor, discount),
            parse_mode="html",
            link_preview=True
        )

        alerts = config.get("star_last_alerts") or {}
        alerts[key] = {
            "price": price,
            "floor": round(floor, 2)
        }
        config["star_last_alerts"] = alerts
        save_config(config)

        star_debug(
            f"ALERT SENT {get_star_gift_title(gift)} "
            f"#{get_star_gift_number(gift)} | {price} Stars"
        )
        return True

    except FloodWaitError as e:
        star_debug(f"CHANNEL FLOODWAIT {e.seconds}")
        await asyncio.sleep(e.seconds + 1)
    except Exception as e:
        star_debug(f"SEND ERROR {type(e).__name__}: {e}")

    return False


async def automatic_star_market_loop():
    next_run = time.monotonic() - STAR_MARKET_INTERVAL

    while True:
        next_run += STAR_MARKET_INTERVAL

        try:
            channel_target = config.get("channel_id") or config.get("channel_username")
            if channel_target:
                gift_types = await get_star_gift_types()

                if gift_types:
                    batch_size = 60
                    cursor = int(config.get("star_scan_cursor", 0) or 0)

                    if cursor >= len(gift_types):
                        cursor = 0

                    batch = gift_types[cursor:cursor + batch_size]
                    if len(batch) < batch_size:
                        batch += gift_types[:batch_size - len(batch)]

                    config["star_scan_cursor"] = (cursor + len(batch)) % len(gift_types)
                    save_config(config)

                    candidates = []

                    for gift in batch:
                        try:
                            candidate = await check_star_gift_type(gift)
                            if candidate:
                                candidates.append(candidate)
                        except Exception as e:
                            star_debug(f"GIFT CHECK ERROR {type(e).__name__}: {e}")
                        await asyncio.sleep(0.15)

                    if candidates:
                        candidates.sort(
                            key=lambda item: (
                                item["price"],
                                -item["discount"]
                            )
                        )
                        await post_star_candidate(candidates[0])

        except Exception as e:
            star_debug(f"STAR LOOP ERROR {type(e).__name__}: {e}")

        delay = next_run - time.monotonic()
        if delay <= 0:
            next_run = time.monotonic()
            continue

        await asyncio.sleep(delay)


async def automatic_random_loop():
    next_run = time.monotonic() - RANDOM_INTERVAL

    while True:
        next_run += RANDOM_INTERVAL
        try:
            channel_target = config.get("channel_id") or config.get("channel_username")
            if channel_target:
                gifts = await fetch_market_gifts()
                gift = select_random_floor_gift(gifts)
                if gift:
                    message = build_single_gift_message(
                        gift,
                        "🎁 <b>Floor Gift</b>"
                    )
                    await client.send_message(
                        channel_target,
                        message,
                        parse_mode="html",
                        link_preview=True
                    )
                    remember_posted_gift(gift)
                    save_config(config)
        except FloodWaitError as e:
            print(f"SINGLE FloodWait: {e.seconds}s")
            await asyncio.sleep(e.seconds)
        except Exception as e:
            print(f"SINGLE LOOP ERROR: {type(e).__name__}: {e}")

        delay = next_run - time.monotonic()
        if delay <= 0:
            next_run = time.monotonic()
            continue
        await asyncio.sleep(delay)


async def automatic_set_loop():
    next_run = time.monotonic() - SET_INTERVAL

    while True:
        next_run += SET_INTERVAL
        try:
            channel_target = config.get("channel_id") or config.get("channel_username")
            if channel_target:
                gifts = await fetch_market_gifts()
                result = select_three_gift_set(gifts)

                if result:
                    signature = set_signature(result)

                    if signature != config.get("last_set_signature"):
                        message = build_set_message(result)
                        await client.send_message(
                            channel_target,
                            message,
                            parse_mode="html",
                            link_preview=True
                        )

                        for gift in result["gifts"]:
                            remember_posted_gift(gift)

                        config["last_set_signature"] = signature
                        save_config(config)
                        print(
                            f"SET POSTED | Background: {result['backdrop']} | "
                            f"Total: {result['total']}"
                        )
                    else:
                        print("SET SKIPPED | Same set as previous cycle")
                else:
                    print("SET CHECK | No complete 3-gift Background set found")

        except FloodWaitError as e:
            print(f"SET FloodWait: {e.seconds}s")
            await asyncio.sleep(e.seconds)
        except Exception as e:
            print(f"SET LOOP ERROR: {type(e).__name__}: {e}")

        delay = next_run - time.monotonic()
        if delay <= 0:
            next_run = time.monotonic()
            continue
        await asyncio.sleep(delay)


async def market_loop():
    print("SCHEDULE | Single=2min | Set=3min | Star=5min")
    await asyncio.gather(
        automatic_random_loop(),
        automatic_set_loop(),
        automatic_star_market_loop(),
        star_rate_loop()
    )


async def connection_health_loop():
    reconnect_delay = 10

    while True:
        try:
            if not client.is_connected():
                print("Telegram disconnected. Reconnecting...")
                await client.connect()

            await client.get_me()

            reconnect_delay = 10

        except FloodWaitError as e:
            print("Health FloodWait:", e.seconds)
            await asyncio.sleep(e.seconds)

        except (ConnectionError, OSError) as e:
            print("Health connection error:", repr(e))
            try:
                await client.disconnect()
            except Exception:
                pass

            await asyncio.sleep(reconnect_delay)
            reconnect_delay = min(reconnect_delay * 2, 300)

        except Exception as e:
            print("Health error:", type(e).__name__, repr(e))

        await asyncio.sleep(60)


@client.on(events.NewMessage(pattern=r"^/panel$"))
async def panel_handler(event):
    if not is_admin(event):
        return

    await event.respond(admin_panel_text(), parse_mode="html")


@client.on(events.NewMessage(pattern=r"^/channel(?:\s+(.+))?$"))
async def channel_handler(event):
    if not is_admin(event):
        return

    value = (event.pattern_match.group(1) or "").strip()
    if not value:
        await event.respond(
            "📢 <b>کانال را وارد کن:</b>\n<code>/channel @ChannelUsername</code>",
            parse_mode="html"
        )
        return

    username = clean_username(value)
    try:
        entity = await client.get_entity(username)

        if not getattr(entity, "broadcast", False):
            await event.respond("❌ این یوزرنیم کانال نیست.")
            return

        channel_id = getattr(entity, "id", None)
        title = getattr(entity, "title", None)
        if not channel_id:
            await event.respond("❌ شناسه کانال دریافت نشد.")
            return

        await client.get_permissions(entity, await client.get_me())

        config["channel_id"] = channel_id
        config["channel_title"] = title
        config["channel_username"] = username
        config["enabled"] = True
        save_config(config)

        await event.respond(
            f"✅ کانال ذخیره شد: <b>{safe_html(title or username)}</b>",
            parse_mode="html"
        )
    except Exception:
        await event.respond("❌ کانال پیدا نشد یا اکانت دسترسی ندارد.")


async def main():
    with contextlib.redirect_stdout(_NULL), contextlib.redirect_stderr(_NULL):
        reconnect_delay = 10

        while True:
            try:
                print("Connecting to Telegram...")
                await client.start()

                star_debug(
                    f"START TELETHON_VERSION={getattr(telethon, '__version__', 'unknown')}"
                )

                me = await client.get_me()

                if not config.get("admin_id"):
                    config["admin_id"] = me.id

                config["enabled"] = True
                config["finder_mode"] = "automatic"
                config["background_mode"] = "all"
                config["background_name"] = None
                config["market"] = DEFAULT_MARKET
                save_config(config)

                print(
                    "Logged in:",
                    me.first_name,
                    f"@{me.username}" if me.username else "",
                    f"| Admin ID: {me.id}"
                )

                if config.get("channel_username") or config.get("channel_id"):
                    print(
                        "Channel:",
                        config.get("channel_title"),
                        config.get("channel_username")
                    )
                else:
                    print("Channel: NOT SET")

                await asyncio.gather(
                    market_loop(),
                    connection_health_loop(),
                    client.run_until_disconnected()
                )

            except FloodWaitError as e:
                print("Main FloodWait:", e.seconds)
                await asyncio.sleep(e.seconds)

            except (ConnectionError, OSError) as e:
                print("Main connection error:", repr(e))

            except Exception as e:
                print("Main loop crashed:", type(e).__name__, repr(e))

            finally:
                try:
                    await client.disconnect()
                except Exception:
                    pass

            print(
                f"Restarting Telegram connection in {reconnect_delay} seconds..."
            )

            await asyncio.sleep(reconnect_delay)
            reconnect_delay = min(reconnect_delay * 2, 300)


if __name__ == "__main__":
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        print("UserBot stopped.")
