# -*- coding: utf-8 -*-
"""
Userbot کمکی (جدا از bot.py اصلی، پروسه‌ی خودشو داره).

با یه اکانت شخصی تلگرام (Telethon، نه Bot API) عضو چند تا چنل موزیک عمومی می‌شه،
آهنگ‌های صوتیِ اون چنل‌ها رو ایندکس (ذخیره‌ی متادیتا تو song_index.json) می‌کنه،
و یه سرور محلیِ کوچیک (فقط رو خودِ همین سرور، غیرقابل‌دسترس از بیرون) بالا میاره که
bot.py اصلی می‌تونه ازش بخواد: "دنبال این آهنگ بگرد، اگه تو ایندکس/چنل‌ها داری بفرست".

نصب پیش‌نیازها:
    pip install telethon aiohttp

اجرای اولیه (باید یه‌بار تعاملی باشه تا لاگین کنی):
    python3 userbot.py
دفعه‌ی اول ازت شماره موبایل، بعد کد تاییدی که تلگرام برات پیامک/تو اپ می‌فرسته، می‌خواد.
بعد از لاگین موفق یه فایل سشن (userbot_session.session) ساخته می‌شه و دیگه لازم نیست
دوباره لاگین کنی؛ فقط دوباره `python3 userbot.py` رو اجرا کن.

⚠️ این پروسه باید همیشه (پس‌زمینه) روشن بمونه، مثل یه سرویس جدا؛ اگه بسته بشه،
bot.py اصلی دیگه نمی‌تونه ازش سرچ بخواد (ولی خودش می‌فهمه و می‌ره سراغ SoundCloud/یوتیوب).
"""
import asyncio
import json
import logging
import os
import re
from difflib import SequenceMatcher
from pathlib import Path

from telethon import TelegramClient, events
from telethon.tl.functions.channels import JoinChannelRequest
from telethon.tl.types import InputMessagesFilterMusic
from telethon.errors import UserAlreadyParticipantError, ChannelPrivateError, FloodWaitError
from aiohttp import web

log = logging.getLogger("userbot")
logging.basicConfig(level=logging.INFO, format="%(asctime)s | %(levelname)s | %(message)s")

# ===================== تنظیمات (این‌ها رو خودت پر کن) =====================
API_ID = 35953490  # از my.telegram.org بگیر (عدده)
API_HASH = "e7dece96039d56be4b413ba4e6ed0eff"  # از my.telegram.org بگیر (رشته‌ست)
PHONE = "+16824138122"  # شماره‌ای که باهاش لاگین می‌کنی، با کد کشور: مثلا "+98912xxxxxxx"

# چنل‌های موزیکی که می‌خوای عضوشون بشه و توشون سرچ/ایندکس کنه (یوزرنیم یا لینک دعوت)
CHANNELS = [
     "@ahangify",
     "@M0_ZK",
]

# هر چند وقت یه‌بار همه‌ی چنل‌ها رو کامل دوباره اسکن کنه (برای آهنگ‌های قدیمی‌تری که
# ایندکس نشدن). واحد: ثانیه. None یعنی فقط یه‌بار موقع استارت.
FULL_RESCAN_INTERVAL = 6 * 3600
# ============================================================================

BASE_DIR = Path(__file__).resolve().parent
SESSION_NAME = str(BASE_DIR / "userbot_session")
INDEX_FILE = BASE_DIR / "song_index.json"
DOWNLOADS_DIR = BASE_DIR / "userbot_downloads"
DOWNLOADS_DIR.mkdir(exist_ok=True)

HTTP_HOST = "127.0.0.1"  # فقط خودِ همین سرور بهش دسترسی داره
HTTP_PORT = 8765
MATCH_THRESHOLD = 0.5  # هرچی به ۱ نزدیک‌تر، سخت‌گیرتر تو تشخیص شباهت اسم آهنگ

client = TelegramClient(SESSION_NAME, API_ID, API_HASH)
song_index: list = []
_index_lock = asyncio.Lock()


def _load_index() -> None:
    global song_index
    if INDEX_FILE.is_file():
        try:
            song_index = json.loads(INDEX_FILE.read_text(encoding="utf-8"))
        except Exception as e:
            log.warning("Could not read %s: %s", INDEX_FILE, e)
            song_index = []


def _save_index() -> None:
    try:
        INDEX_FILE.write_text(json.dumps(song_index, ensure_ascii=False, indent=2), encoding="utf-8")
    except Exception as e:
        log.warning("Could not save index: %s", e)


def _normalize(text: str) -> str:
    return re.sub(r"[^\w\s]", " ", text.lower()).strip()


def _entry_texts(entry: dict) -> list:
    texts = []
    tag = f"{entry.get('performer') or ''} {entry.get('title') or ''}".strip()
    if tag:
        texts.append(tag)
    if entry.get("file_name"):
        texts.append(os.path.splitext(entry["file_name"])[0])
    return texts


def _best_match(query: str):
    query_norm = _normalize(query)
    best_entry, best_score = None, 0.0
    for entry in song_index:
        for text in _entry_texts(entry):
            score = SequenceMatcher(None, query_norm, _normalize(text)).ratio()
            if score > best_score:
                best_score, best_entry = score, entry
    if best_entry is not None and best_score >= MATCH_THRESHOLD:
        return best_entry, best_score
    return None, 0.0


def _extract_audio_meta(message) -> dict:
    title, performer = None, None
    doc = message.audio
    for attr in getattr(doc, "attributes", []) or []:
        if getattr(attr, "title", None):
            title = attr.title
        if getattr(attr, "performer", None):
            performer = attr.performer
    return {"title": title, "performer": performer}


async def _add_to_index_if_music(message, channel_key: str) -> bool:
    if not getattr(message, "audio", None):
        return False
    for e in song_index:
        if e["channel"] == channel_key and e["msg_id"] == message.id:
            return False  # از قبل هست
    meta = _extract_audio_meta(message)
    song_index.append({
        "channel": channel_key,
        "msg_id": message.id,
        "title": meta["title"],
        "performer": meta["performer"],
        "file_name": getattr(message.file, "name", None) if message.file else None,
    })
    return True


async def index_channel(channel_key: str, limit: int = 5000) -> None:
    log.info("Indexing %s ...", channel_key)
    try:
        entity = await client.get_entity(channel_key)
    except Exception as e:
        log.warning("Could not resolve %s: %s", channel_key, e)
        return
    added = 0
    try:
        async for message in client.iter_messages(entity, filter=InputMessagesFilterMusic, limit=limit):
            if await _add_to_index_if_music(message, channel_key):
                added += 1
    except FloodWaitError as e:
        log.warning("FloodWait %ss while indexing %s, will continue next cycle", e.seconds, channel_key)
        return
    except Exception as e:
        log.warning("Error indexing %s: %s", channel_key, e)
        return
    if added:
        _save_index()
    log.info("Indexed %d new audio messages from %s (total now considered)", added, channel_key)


async def join_channel(channel_key: str) -> None:
    try:
        await client(JoinChannelRequest(channel_key))
        log.info("Joined %s", channel_key)
    except UserAlreadyParticipantError:
        pass
    except ChannelPrivateError:
        log.warning("%s is private/invalid, skipping", channel_key)
    except FloodWaitError as e:
        log.warning("FloodWait %ss while joining %s", e.seconds, channel_key)
    except Exception as e:
        log.warning("Join failed for %s: %s", channel_key, e)


async def join_and_index_all() -> None:
    for ch in CHANNELS:
        await join_channel(ch)
        await index_channel(ch)


async def periodic_rescan() -> None:
    if not FULL_RESCAN_INTERVAL:
        return
    while True:
        await asyncio.sleep(FULL_RESCAN_INTERVAL)
        log.info("Starting periodic full rescan...")
        await join_and_index_all()


@client.on(events.NewMessage(chats=CHANNELS if CHANNELS else None))
async def on_new_message(event) -> None:
    if not CHANNELS:
        return
    channel_key = event.chat.username and f"@{event.chat.username}" or str(event.chat_id)
    async with _index_lock:
        if await _add_to_index_if_music(event.message, channel_key):
            _save_index()


async def _live_search(query: str):
    """اگه تو ایندکس نبود، مستقیم تو چنل‌ها سرچ زنده بزن (کندتره ولی تازه‌تره)."""
    for ch in CHANNELS:
        try:
            entity = await client.get_entity(ch)
            async for message in client.iter_messages(entity, search=query,
                                                        filter=InputMessagesFilterMusic, limit=5):
                await _add_to_index_if_music(message, ch)
        except FloodWaitError as e:
            log.warning("FloodWait %ss during live search on %s", e.seconds, ch)
        except Exception as e:
            log.warning("Live search failed on %s: %s", ch, e)
    _save_index()
    return _best_match(query)


async def handle_find(request: web.Request) -> web.Response:
    title = request.query.get("title", "").strip()
    artist = request.query.get("artist", "").strip()
    query = f"{artist} {title}".strip() or title
    if not query:
        return web.json_response({"found": False, "error": "missing title"}, status=400)

    async with _index_lock:
        entry, score = _best_match(query)
        if entry is None:
            entry, score = await _live_search(query)

    if entry is None:
        return web.json_response({"found": False})

    try:
        entity = await client.get_entity(entry["channel"])
        message = await client.get_messages(entity, ids=entry["msg_id"])
        if message is None or not getattr(message, "audio", None):
            return web.json_response({"found": False})
        safe_channel = re.sub(r"\W+", "_", str(entry["channel"]))
        out_path = DOWNLOADS_DIR / f"{safe_channel}_{entry['msg_id']}.mp3"
        if not out_path.is_file():
            await client.download_media(message, file=str(out_path))
    except Exception as e:
        log.warning("Download failed for match %s: %s", entry, e)
        return web.json_response({"found": False})

    return web.json_response({
        "found": True,
        "path": str(out_path),
        "score": round(score, 2),
        "title": entry.get("title"),
        "performer": entry.get("performer"),
    })


async def handle_health(request: web.Request) -> web.Response:
    return web.json_response({"ok": True, "indexed": len(song_index), "channels": len(CHANNELS)})


async def main() -> None:
    if not API_ID or not API_HASH or not PHONE:
        log.error("API_ID/API_HASH/PHONE رو بالای فایل پر کن (از my.telegram.org بگیر) و دوباره اجرا کن.")
        return
    if not CHANNELS:
        log.warning("لیست CHANNELS خالیه؛ فایل رو ویرایش کن و یوزرنیم چنل‌ها رو اضافه کن.")

    _load_index()
    await client.start(phone=PHONE)
    log.info("Userbot logged in.")

    await join_and_index_all()
    asyncio.create_task(periodic_rescan())

    app = web.Application()
    app.router.add_get("/find", handle_find)
    app.router.add_get("/health", handle_health)
    runner = web.AppRunner(app)
    await runner.setup()
    site = web.TCPSite(runner, HTTP_HOST, HTTP_PORT)
    await site.start()
    log.info("Local search server ready: http://%s:%s/find?title=...&artist=...", HTTP_HOST, HTTP_PORT)

    await client.run_until_disconnected()


if __name__ == "__main__":
    asyncio.run(main())
