#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Telegram Bot for YouTube & Aparat notifications - Cron-safe single-run version
نسخه اصلاح‌شده: escape امن HTML، پردازش همه ویدیوهای جدید،
ذخیره وضعیت فقط بعد از ارسال موفق، حذف کاربران بلاک‌شده، parser امن XML
"""

import os
import sys
import re
import html
import json
import time
import traceback
import fcntl
import tempfile
import xml.etree.ElementTree as ET
from datetime import datetime
from typing import Dict, List, Optional, Any

try:
    import requests
except ImportError:
    print("ERROR: 'requests' module not found. Install it: pip3 install requests")
    sys.exit(1)

try:
    import config
except ImportError:
    print("ERROR: config.py not found in the same directory as bot.py")
    sys.exit(1)

# ==================== مسیرها و ثابت‌ها ====================
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
LOCK_FILE = os.path.join(SCRIPT_DIR, ".bot.lock")
ERROR_LOG = os.path.join(SCRIPT_DIR, "bot_error.log")

SUBSCRIBERS_FILE = os.path.join(SCRIPT_DIR, config.SUBSCRIBERS_FILE)
STATE_FILE = os.path.join(SCRIPT_DIR, config.STATE_FILE)

BOT_TOKEN = config.BOT_TOKEN
APARAT_USERNAME = getattr(config, "APARAT_USERNAME", "")
YOUTUBE_CHANNEL_URL = getattr(config, "YOUTUBE_CHANNEL_URL", "")
REQUEST_TIMEOUT = getattr(config, "REQUEST_TIMEOUT", 15)
USER_AGENT = getattr(config, "USER_AGENT", "Mozilla/5.0")
SEND_TO_PRIVATE = getattr(config, "SEND_TO_PRIVATE", True)
SEND_TO_GROUPS = getattr(config, "SEND_TO_GROUPS", True)
SEND_TO_CHANNELS = getattr(config, "SEND_TO_CHANNELS", True)

TELEGRAM_API = f"https://api.telegram.org/bot{BOT_TOKEN}"

# ==================== لاگ خطا ====================
def log_error(message: str):
    """ذخیره خطا در فایل لاگ با timestamp، همراه با traceback کامل در صورت وجود"""
    timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    try:
        with open(ERROR_LOG, "a", encoding="utf-8") as f:
            f.write(f"[{timestamp}] {message}\n")
            f.write(traceback.format_exc())
            f.write("\n")
    except Exception as e:
        print(f"Failed to write error log: {e}")

# ==================== ابزار نوشتن Atomic و امن ====================
def atomic_write_json(path: str, data: Any):
    """نوشتن امن JSON: اگر نوشتن fail کنه، فایل موقت پاک می‌شه و فایل اصلی دست‌نخورده می‌مونه"""
    temp_path = None
    try:
        temp_fd, temp_path = tempfile.mkstemp(dir=SCRIPT_DIR, text=True)
        with os.fdopen(temp_fd, "w", encoding="utf-8") as f:
            json.dump(data, f, ensure_ascii=False, indent=2)
        os.replace(temp_path, path)
        temp_path = None
    except Exception as e:
        log_error(f"Error writing {path}: {e}")
    finally:
        if temp_path and os.path.exists(temp_path):
            try:
                os.remove(temp_path)
            except Exception:
                pass

# ==================== مدیریت subscribers.json ====================
def load_subscribers() -> Dict[str, Dict]:
    if not os.path.exists(SUBSCRIBERS_FILE):
        return {}
    try:
        with open(SUBSCRIBERS_FILE, "r", encoding="utf-8") as f:
            data = json.load(f)
            return data if isinstance(data, dict) else {}
    except Exception as e:
        log_error(f"Error loading subscribers: {e}")
        return {}

def save_subscribers(subs: Dict[str, Dict]):
    atomic_write_json(SUBSCRIBERS_FILE, subs)

def add_subscriber(chat_id: int, chat_type: str, user_info: Optional[Dict] = None) -> bool:
    """اضافه کردن مشترک جدید. True یعنی جدید بود، False یعنی قبلاً بوده"""
    subs = load_subscribers()
    chat_id_str = str(chat_id)

    if chat_id_str in subs:
        # اگه قبلاً غیرفعال شده بود (مثلاً بلاک کرده بود)، دوباره فعالش کن
        subs[chat_id_str]["active"] = True
        save_subscribers(subs)
        return False

    subs[chat_id_str] = {
        "id": chat_id,
        "type": chat_type,
        "added_at": datetime.now().isoformat(),
        "user_info": user_info or {},
        "active": True
    }
    save_subscribers(subs)
    return True

def remove_subscriber(chat_id_str: str):
    """حذف/غیرفعال کردن کاربری که ربات رو بلاک کرده"""
    subs = load_subscribers()
    if chat_id_str in subs:
        subs[chat_id_str]["active"] = False
        subs[chat_id_str]["removed_at"] = datetime.now().isoformat()
        save_subscribers(subs)
        log_error(f"Subscriber {chat_id_str} marked inactive (blocked bot)")

# ==================== مدیریت bot_state.json ====================
def load_state() -> Dict[str, Any]:
    default_state = {
        "telegram_offset": 0,
        "youtube_channel_id": None,
        "youtube_last_video_id": None,
        "aparat_last_video_id": None,
        "last_check_at": None
    }

    if not os.path.exists(STATE_FILE):
        return default_state

    try:
        with open(STATE_FILE, "r", encoding="utf-8") as f:
            data = json.load(f)
            if isinstance(data, dict):
                return {**default_state, **data}
            return default_state
    except Exception as e:
        log_error(f"Error loading state: {e}")
        return default_state

def save_state(state: Dict[str, Any]):
    atomic_write_json(STATE_FILE, state)

# ==================== تلگرام: دریافت و ارسال ====================
def send_message(chat_id: int, text: str) -> tuple:
    """
    ارسال یک پیام به یک چت خاص.
    خروجی: (موفق بود؟, کاربر بلاک کرده؟)
    """
    try:
        resp = requests.post(
            f"{TELEGRAM_API}/sendMessage",
            json={"chat_id": chat_id, "text": text, "parse_mode": "HTML"},
            timeout=REQUEST_TIMEOUT
        )
        if resp.status_code == 200:
            return True, False

        # بررسی اینکه آیا کاربر ربات رو بلاک کرده (403 Forbidden)
        if resp.status_code == 403:
            return False, True

        log_error(f"sendMessage failed for {chat_id}: {resp.status_code} - {resp.text[:200]}")
        return False, False
    except Exception as e:
        log_error(f"Error sending message to {chat_id}: {e}")
        return False, False

def broadcast(text: str) -> bool:
    """
    ارسال پیام به تمام مشترکین فعال با فیلتر نوع چت.
    خروجی: True اگر حداقل یک ارسال موفق بود (یا اصلاً مشترکی وجود نداشت)
    """
    subs = load_subscribers()
    sent_count = 0
    fail_count = 0
    blocked_ids = []

    active_subs = {cid: info for cid, info in subs.items() if info.get("active", True)}

    if not active_subs:
        log_error("Broadcast: no active subscribers")
        return True  # چیزی برای ارسال نبود، خطایی هم نیست

    for chat_id_str, info in active_subs.items():
        chat_type = info.get("type", "private")

        if chat_type == "private" and not SEND_TO_PRIVATE:
            continue
        if chat_type in ("group", "supergroup") and not SEND_TO_GROUPS:
            continue
        if chat_type == "channel" and not SEND_TO_CHANNELS:
            continue

        success, blocked = send_message(int(chat_id_str), text)
        if success:
            sent_count += 1
        else:
            fail_count += 1
            if blocked:
                blocked_ids.append(chat_id_str)
        time.sleep(0.05)

    # حذف کاربرانی که ربات رو بلاک کردن
    for cid in blocked_ids:
        remove_subscriber(cid)

    log_error(f"Broadcast sent to {sent_count} subscribers, {fail_count} failed, {len(blocked_ids)} blocked-and-removed")

    # اگه هیچ‌کدوم موفق نشدن و مشترک فعالی وجود داشت، شکست‌خورده در نظر بگیر
    if sent_count == 0 and fail_count > 0:
        return False
    return True

def get_updates(offset: int) -> List[Dict]:
    try:
        resp = requests.post(
            f"{TELEGRAM_API}/getUpdates",
            json={"offset": offset, "timeout": 10, "allowed_updates": ["message"]},
            timeout=REQUEST_TIMEOUT + 5
        )
        if resp.status_code == 200:
            result = resp.json()
            return result.get("result", [])
        else:
            log_error(f"getUpdates failed: {resp.status_code}")
            return []
    except Exception as e:
        log_error(f"Error getting updates: {e}")
        return []

def process_telegram_updates(state: Dict):
    """پردازش پیام‌های جدید تلگرام و ثبت‌نام کاربران"""
    offset = state.get("telegram_offset", 0)
    updates = get_updates(offset)

    if not updates:
        return

    max_update_id = offset - 1
    any_registration_failed = False

    for update in updates:
        update_id = update.get("update_id", 0)
        if update_id > max_update_id:
            max_update_id = update_id

        message = update.get("message")
        if not message:
            continue

        chat = message.get("chat", {})
        chat_id = chat.get("id")
        chat_type = chat.get("type", "private")
        text = (message.get("text") or "").strip()

        if not chat_id:
            continue

        if text == "/start":
            user_info = {
                "first_name": chat.get("first_name"),
                "last_name": chat.get("last_name"),
                "username": chat.get("username"),
                "title": chat.get("title")
            }

            try:
                is_new = add_subscriber(chat_id, chat_type, user_info)
            except Exception as e:
                log_error(f"Failed to register subscriber {chat_id}: {e}")
                any_registration_failed = True
                continue

            if is_new:
                welcome_msg = (
                    "🎉 <b>خوش آمدید!</b>\n\n"
                    "شما با موفقیت عضو شدید.\n"
                    "به محض انتشار ویدیو یا لایو جدید در یوتیوب و آپارات، "
                    "به شما اطلاع می‌دهیم. ✅"
                )
            else:
                welcome_msg = (
                    "👋 <b>خوش برگشتید!</b>\n\n"
                    "شما قبلاً عضو شده‌اید و هر ویدیو جدید رو دریافت می‌کنید. 😊"
                )

            send_message(chat_id, welcome_msg)

    # فقط اگه همه‌چیز درست پیش رفت، offset رو جلو ببر
    # (اگه ثبت‌نام کسی fail شد، offset رو جلو نمی‌بریم تا دفعه بعد دوباره تلاش بشه)
    if not any_registration_failed:
        state["telegram_offset"] = max_update_id + 1
        save_state(state)
    else:
        log_error("Some registrations failed, offset not advanced - will retry next run")

# ==================== YouTube: استخراج Channel ID از Handle ====================
def extract_youtube_channel_id(handle_url: str) -> Optional[str]:
    try:
        headers = {"User-Agent": USER_AGENT}
        resp = requests.get(handle_url, headers=headers, timeout=REQUEST_TIMEOUT)

        if resp.status_code != 200:
            return None

        page_html = resp.text

        match = re.search(r'"channelId":"(UC[\w-]+)"', page_html)
        if match:
            return match.group(1)

        match = re.search(r'/channel/(UC[\w-]+)', page_html)
        if match:
            return match.group(1)

        match = re.search(r'"externalId":"(UC[\w-]+)"', page_html)
        if match:
            return match.group(1)

        return None
    except Exception as e:
        log_error(f"Error extracting YouTube channel ID: {e}")
        return None

# ==================== YouTube: بررسی RSS Feed (با XML parser امن) ====================
YT_NS = {
    "atom": "http://www.w3.org/2005/Atom",
    "yt": "http://www.youtube.com/xml/schemas/2015"
}

def parse_youtube_rss(xml_text: str) -> List[Dict]:
    """
    پارس امن فید RSS یوتیوب با xml.etree.ElementTree
    خروجی: لیست ویدیوها به ترتیب از جدید به قدیم
    """
    videos = []
    try:
        root = ET.fromstring(xml_text)
        entries = root.findall("atom:entry", YT_NS)

        for entry in entries:
            video_id_el = entry.find("yt:videoId", YT_NS)
            title_el = entry.find("atom:title", YT_NS)
            link_el = entry.find("atom:link", YT_NS)

            if video_id_el is None or video_id_el.text is None:
                continue

            video_id = video_id_el.text.strip()
            title = title_el.text.strip() if (title_el is not None and title_el.text) else "ویدیوی جدید"
            link = link_el.get("href") if link_el is not None else f"https://www.youtube.com/watch?v={video_id}"

            videos.append({"id": video_id, "title": title, "link": link})
    except ET.ParseError as e:
        log_error(f"YouTube RSS XML parse error: {e}")
    return videos

def check_youtube(state: Dict):
    """بررسی ویدیوهای جدید یوتیوب - همه ویدیوهای جدید رو پردازش می‌کنه، نه فقط آخری"""
    if not YOUTUBE_CHANNEL_URL:
        return

    channel_id = state.get("youtube_channel_id")

    if not channel_id:
        log_error("Extracting YouTube Channel ID from handle URL...")
        channel_id = extract_youtube_channel_id(YOUTUBE_CHANNEL_URL)
        if channel_id:
            state["youtube_channel_id"] = channel_id
            save_state(state)
            log_error(f"YouTube Channel ID extracted: {channel_id}")
        else:
            log_error("Failed to extract YouTube Channel ID. Skipping YouTube check.")
            return

    rss_url = f"https://www.youtube.com/feeds/videos.xml?channel_id={channel_id}"

    try:
        headers = {"User-Agent": USER_AGENT}
        resp = requests.get(rss_url, headers=headers, timeout=REQUEST_TIMEOUT)

        if resp.status_code != 200:
            log_error(f"YouTube RSS fetch failed: {resp.status_code}")
            return

        videos = parse_youtube_rss(resp.text)
        if not videos:
            return

        last_video_id = state.get("youtube_last_video_id")

        # اولین اجرا: فقط ذخیره کن، اعلان نفرست (جلوگیری از spam ویدیوهای قدیمی)
        if last_video_id is None:
            state["youtube_last_video_id"] = videos[0]["id"]
            save_state(state)
            log_error("YouTube: First run, saved latest video ID without notification")
            return

        # پیدا کردن جایگاه آخرین ویدیوی شناخته‌شده در لیست
        new_videos = []
        for v in videos:
            if v["id"] == last_video_id:
                break
            new_videos.append(v)

        if not new_videos:
            return  # چیز جدیدی نیست

        # ارسال از قدیمی‌ترین به جدیدترین (ترتیب طبیعی انتشار)
        new_videos.reverse()

        newest_sent_id = state.get("youtube_last_video_id")
        for v in new_videos:
            safe_title = html.escape(v["title"])
            is_live = "live" in v["title"].lower() or "🔴" in v["title"]

            if is_live:
                msg = f"🔴 <b>لایو استریم جدید در یوتیوب!</b>\n\n📺 {safe_title}\n\n🔗 {v['link']}"
            else:
                msg = f"🎬 <b>ویدیوی جدید در یوتیوب!</b>\n\n📹 {safe_title}\n\n🔗 {v['link']}"

            ok = broadcast(msg)
            if ok:
                newest_sent_id = v["id"]
                state["youtube_last_video_id"] = newest_sent_id
                save_state(state)  # بعد از هر ارسال موفق فوراً ذخیره کن
            else:
                log_error(f"YouTube broadcast failed for video {v['id']}, will retry next run")
                break  # اگه یکی fail شد، بقیه رو هم نگه‌دار برای دفعه بعد

    except Exception as e:
        log_error(f"Error checking YouTube: {e}")

# ==================== Aparat: بررسی API ====================
def check_aparat(state: Dict):
    """بررسی ویدیوهای جدید آپارات"""
    if not APARAT_USERNAME:
        return

    api_url = f"https://www.aparat.com/api/fa/v1/user/user/videobycategory/username/{APARAT_USERNAME}/perpage/10"

    try:
        headers = {"User-Agent": USER_AGENT}
        resp = requests.get(api_url, headers=headers, timeout=REQUEST_TIMEOUT)

        if resp.status_code != 200:
            log_error(f"Aparat API fetch failed: {resp.status_code}")
            return

        data = resp.json()
        included = data.get("included", [])

        if not included:
            return

        videos = []
        for item in included:
            vid = item.get("id")
            attrs = item.get("attributes", {})
            title = attrs.get("title", "ویدیوی جدید")
            is_live = attrs.get("live_at") is not None
            videos.append({
                "id": vid,
                "title": title,
                "link": f"https://www.aparat.com/v/{vid}",
                "is_live": is_live
            })

        last_video_id = state.get("aparat_last_video_id")

        if last_video_id is None:
            state["aparat_last_video_id"] = videos[0]["id"]
            save_state(state)
            log_error("Aparat: First run, saved latest video ID without notification")
            return

        new_videos = []
        for v in videos:
            if v["id"] == last_video_id:
                break
            new_videos.append(v)

        if not new_videos:
            return

        new_videos.reverse()

        for v in new_videos:
            safe_title = html.escape(v["title"])

            if v["is_live"]:
                msg = f"🔴 <b>لایو استریم جدید در آپارات!</b>\n\n📺 {safe_title}\n\n🔗 {v['link']}"
            else:
                msg = f"🎬 <b>ویدیوی جدید در آپارات!</b>\n\n📹 {safe_title}\n\n🔗 {v['link']}"

            ok = broadcast(msg)
            if ok:
                state["aparat_last_video_id"] = v["id"]
                save_state(state)
            else:
                log_error(f"Aparat broadcast failed for video {v['id']}, will retry next run")
                break

    except Exception as e:
        log_error(f"Error checking Aparat: {e}")

# ==================== اجرای اصلی ====================
def main():
    """تابع اصلی - یک چرخه کامل بررسی"""
    state = load_state()

    process_telegram_updates(state)
    check_youtube(state)
    check_aparat(state)

    state["last_check_at"] = datetime.now().isoformat()
    save_state(state)

    log_error("Bot cycle completed successfully")

# ==================== ورودی اصلی با قفل ====================
if __name__ == "__main__":
    lock_fd = None
    try:
        lock_fd = open(LOCK_FILE, "w")
        fcntl.flock(lock_fd.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
        main()

    except BlockingIOError:
        print("Another instance is already running. Exiting.")
    except Exception as e:
        log_error(f"Fatal error: {e}")
        print(f"Error: {e}")
    finally:
        if lock_fd:
            try:
                fcntl.flock(lock_fd.fileno(), fcntl.LOCK_UN)
                lock_fd.close()
            except Exception:
                pass
