import asyncio
import logging
import os
import re
import shutil
import time
import uuid
from pathlib import Path
from urllib.parse import urlparse, unquote

import aiohttp
from telegram import Update
from telegram.constants import ChatAction
from telegram.ext import (
    Application,
    CommandHandler,
    ContextTypes,
    MessageHandler,
    filters,
)

from config import (
    BOT_TOKEN,
    ADMIN_IDS,
    DOWNLOAD_DIR,
    OUTPUT_DIR,
    MAX_DOWNLOAD_MB,
)

# Local Telegram Bot API
BOT_API_BASE = os.getenv("BOT_API_BASE", "http://127.0.0.1:8081/bot")
BOT_FILE_BASE = os.getenv("BOT_FILE_BASE", "http://127.0.0.1:8081/file/bot")

VIDEO_EXTS = {
    ".mp4", ".mkv", ".mov", ".avi", ".webm", ".m4v", ".ts", ".flv"
}
SUB_EXTS = {".srt", ".vtt", ".ass", ".ssa"}

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

# Per-chat job state.
JOBS = {}
LOCKS = {}


def is_admin(user_id: int) -> bool:
    return user_id in ADMIN_IDS


def safe_name(name: str, fallback: str = "file") -> str:
    name = unquote(name or "").strip()
    name = Path(name).name
    name = re.sub(r"[^\w\-. ()\[\]آ-ی]", "_", name, flags=re.UNICODE)
    name = re.sub(r"_+", "_", name).strip(" ._")
    return (name[:180] or fallback)


def job_dir(chat_id: int) -> Path:
    p = DOWNLOAD_DIR / f"job_{chat_id}_{uuid.uuid4().hex[:10]}"
    p.mkdir(parents=True, exist_ok=True)
    return p


def get_job(chat_id: int):
    return JOBS.get(chat_id)


def cleanup_job(job):
    if not job:
        return
    workdir = job.get("workdir")
    if workdir:
        try:
            shutil.rmtree(workdir, ignore_errors=True)
        except Exception:
            pass

    output = job.get("output")
    if output:
        try:
            Path(output).unlink(missing_ok=True)
        except Exception:
            pass


async def ensure_admin(update: Update) -> bool:
    user = update.effective_user
    if not user or not is_admin(user.id):
        if update.message:
            await update.message.reply_text("⛔️ دسترسی ندارید.")
        return False
    return True


async def safe_edit(message, text):
    try:
        await message.edit_text(text)
    except Exception:
        pass


def progress_bar(pct: float, width: int = 20) -> str:
    pct = max(0.0, min(100.0, pct))
    filled = int(width * pct / 100)
    return "█" * filled + "░" * (width - filled)


async def download_direct(url: str, destination: Path, status_message=None):
    parsed = urlparse(url)
    if parsed.scheme not in ("http", "https"):
        raise ValueError("فقط لینک‌های http و https قابل دانلود هستند.")

    limit = int(MAX_DOWNLOAD_MB) * 1024 * 1024
    timeout = aiohttp.ClientTimeout(total=None, sock_connect=30, sock_read=120)

    headers = {"User-Agent": "Mozilla/5.0 HardSubBot/1.0"}

    async with aiohttp.ClientSession(timeout=timeout, headers=headers) as session:
        async with session.get(url, allow_redirects=True) as response:
            response.raise_for_status()

            content_length = response.headers.get("Content-Length")
            if content_length:
                try:
                    if int(content_length) > limit:
                        raise ValueError(
                            f"حجم فایل بیشتر از {MAX_DOWNLOAD_MB}MB است."
                        )
                except ValueError as e:
                    if "حجم فایل" in str(e):
                        raise

            total = 0
            last_update = 0.0

            with destination.open("wb") as out:
                async for chunk in response.content.iter_chunked(1024 * 1024):
                    total += len(chunk)

                    if total > limit:
                        raise ValueError(
                            f"حجم فایل بیشتر از {MAX_DOWNLOAD_MB}MB است."
                        )

                    out.write(chunk)

                    now = time.monotonic()
                    if (
                        status_message
                        and content_length
                        and now - last_update > 2
                    ):
                        try:
                            pct = total / int(content_length) * 100
                            await safe_edit(
                                status_message,
                                f"⬇️ دانلود فایل...\n"
                                f"{progress_bar(pct)} {pct:.1f}%",
                            )
                        except Exception:
                            pass
                        last_update = now

    return destination


async def download_telegram_file(message, context, destination: Path):
    target = message.document or message.video or message.audio
    if not target:
        raise ValueError("فایل قابل دریافت پیدا نشد.")

    if target.file_size and target.file_size > int(MAX_DOWNLOAD_MB) * 1024 * 1024:
        raise ValueError(f"حجم فایل بیشتر از {MAX_DOWNLOAD_MB}MB است.")

    tg_file = await context.bot.get_file(target.file_id)
    await tg_file.download_to_drive(custom_path=str(destination))
    return destination


async def get_media_from_message(update, context, workdir, status):
    msg = update.message

    if msg.text and re.match(r"^https?://", msg.text.strip(), re.I):
        url = msg.text.strip()
        parsed = urlparse(url)
        filename = safe_name(
            Path(parsed.path).name or f"video_{int(time.time())}.mp4",
            "video.mp4",
        )
        destination = workdir / filename

        await safe_edit(status, "⬇️ در حال دانلود لینک...")
        return await download_direct(url, destination, status)

    if msg.document or msg.video:
        target = msg.document or msg.video
        original = getattr(target, "file_name", None) or "video.mkv"
        filename = safe_name(original, "video.mkv")
        destination = workdir / filename

        await safe_edit(status, "⬇️ در حال دریافت فایل از تلگرام...")
        await download_telegram_file(msg, context, destination)
        return destination

    return None


async def run_cmd(cmd, timeout=None):
    log.info("Running: %s", " ".join(map(str, cmd)))

    proc = await asyncio.create_subprocess_exec(
        *map(str, cmd),
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.PIPE,
    )

    try:
        stdout, stderr = await asyncio.wait_for(
            proc.communicate(), timeout=timeout
        )
    except asyncio.TimeoutError:
        proc.kill()
        await proc.wait()
        raise RuntimeError("اجرای FFmpeg/FFprobe بیش از زمان مجاز طول کشید.")

    if proc.returncode != 0:
        error = stderr.decode(errors="replace").strip()
        raise RuntimeError(error[-5000:] or "اجرای دستور ناموفق بود.")

    return stdout, stderr


async def ffprobe_duration(video: Path):
    cmd = [
        "ffprobe",
        "-v", "error",
        "-show_entries", "format=duration",
        "-of", "default=noprint_wrappers=1:nokey=1",
        str(video),
    ]

    stdout, _ = await run_cmd(cmd, timeout=60)

    try:
        return float(stdout.decode().strip())
    except Exception:
        return None


async def extract_internal_subtitle(video: Path, output: Path):
    cmd = [
        "ffprobe",
        "-v", "error",
        "-select_streams", "s",
        "-show_entries", "stream=index:stream_tags=language,title",
        "-of", "json",
        str(video),
    ]

    stdout, _ = await run_cmd(cmd, timeout=60)

    import json
    data = json.loads(stdout.decode(errors="replace") or "{}")
    streams = data.get("streams", [])

    if not streams:
        raise ValueError("هیچ ترک زیرنویس داخلی در ویدئو پیدا نشد.")

    # Prefer Persian/English, otherwise first subtitle stream.
    selected = streams[0]["index"]
    preferred = {"fa", "fas", "per", "en", "eng"}

    for stream in streams:
        tags = stream.get("tags") or {}
        language = str(tags.get("language", "")).lower()
        if language in preferred:
            selected = stream["index"]
            break

    cmd = [
        "ffmpeg",
        "-y",
        "-hide_banner",
        "-loglevel", "error",
        "-i", str(video),
        "-map", f"0:{selected}",
        "-c:s", "srt",
        str(output),
    ]

    await run_cmd(cmd, timeout=300)
    return output


def escape_subtitle_filter(path: Path) -> str:
    # FFmpeg subtitles filter escaping.
    p = str(path.resolve()).replace("\\", "/")
    p = p.replace("\\", r"\\")
    p = p.replace(":", r"\:")
    p = p.replace("'", r"\'")
    p = p.replace("[", r"\[")
    p = p.replace("]", r"\]")
    return p


async def hardsub(video: Path, subtitle: Path, output: Path, status):
    duration = await ffprobe_duration(video)

    sub = escape_subtitle_filter(subtitle)

    vf = f"subtitles=filename='{sub}'"

    cmd = [
        "ffmpeg",
        "-y",
        "-hide_banner",
        "-loglevel", "error",
        "-i", str(video),
        "-vf", vf,
        "-map", "0:v:0",
        "-map", "0:a?",
        "-c:v", "libx264",
        "-preset", "veryfast",
        "-crf", "20",
        "-c:a", "aac",
        "-b:a", "128k",
        "-movflags", "+faststart",
        "-progress", "pipe:1",
        "-nostats",
        str(output),
    ]

    log.info("Starting hardsub: %s", output)

    proc = await asyncio.create_subprocess_exec(
        *cmd,
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.PIPE,
    )

    last_update = 0.0
    stderr_task = asyncio.create_task(proc.stderr.read())

    try:
        while True:
            line = await proc.stdout.readline()

            if not line:
                break

            text = line.decode(errors="ignore").strip()

            if text.startswith("out_time_ms=") and duration:
                try:
                    out_ms = int(text.split("=", 1)[1])
                    pct = min(
                        99.0,
                        max(
                            0.0,
                            (out_ms / 1_000_000) / duration * 100,
                        ),
                    )

                    now = time.monotonic()

                    if now - last_update >= 3:
                        await safe_edit(
                            status,
                            f"🎞 هاردساب در حال انجام...\n"
                            f"{progress_bar(pct)} {pct:.1f}%",
                        )
                        last_update = now

                except Exception:
                    pass

        stderr = await stderr_task
        rc = await proc.wait()

    except Exception:
        proc.kill()
        await proc.wait()
        raise

    if rc != 0:
        error = stderr.decode(errors="replace").strip()
        raise RuntimeError(
            f"FFmpeg failed:\n{error[-5000:]}"
        )

    await safe_edit(
        status,
        f"🎞 هاردساب در حال انجام...\n"
        f"{progress_bar(100)} 100%",
    )

    if not output.exists() or output.stat().st_size == 0:
        raise RuntimeError("فایل خروجی ساخته نشد.")

    return output


async def send_output(context, chat_id, output: Path, caption):
    # PTB uses the configured local Bot API base URLs.
    with output.open("rb") as f:
        await context.bot.send_document(
            chat_id=chat_id,
            document=f,
            caption=caption[:1024],
            read_timeout=600,
            write_timeout=600,
            connect_timeout=30,
            pool_timeout=30,
        )


async def process_job(update, context, video: Path, subtitle: Path):
    msg = update.message
    chat_id = msg.chat_id

    if LOCKS.get(chat_id):
        await msg.reply_text("⏳ یک عملیات دیگر برای این چت در حال اجراست.")
        return

    LOCKS[chat_id] = True

    workdir = video.parent
    output = workdir / f"{video.stem}_hardsub.mkv"

    status = await msg.reply_text("⚙️ آماده‌سازی هاردساب...")

    job = {
        "workdir": str(workdir),
        "video": str(video),
        "subtitle": str(subtitle),
        "output": str(output),
    }
    JOBS[chat_id] = job

    try:
        await msg.chat.send_action(ChatAction.UPLOAD_DOCUMENT)

        await hardsub(video, subtitle, output, status)

        await safe_edit(
            status,
            "📤 هاردساب تمام شد.\n"
            "در حال ارسال فایل خروجی...",
        )

        await send_output(
            context,
            chat_id,
            output,
            f"🎬 هاردساب آماده شد: {output.name}",
        )

        await safe_edit(
            status,
            "✅ فایل با موفقیت تحویل داده شد.\n"
            "🧹 فایل‌های موقت از سرور حذف شدند.",
        )

        # Successful delivery: remove everything related to this job.
        cleanup_job(job)

    except Exception as e:
        log.exception("process_job")
        await safe_edit(
            status,
            f"❌ عملیات ناموفق بود:\n{str(e)[-2500:]}",
        )

        # Also clean files after failure.
        cleanup_job(job)

    finally:
        JOBS.pop(chat_id, None)
        LOCKS.pop(chat_id, None)


async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
    if not await ensure_admin(update):
        return

    await update.message.reply_text(
        "سلام 👋\n\n"
        "🎬 فایل ویدئو یا لینک مستقیم ویدئو را بفرست.\n"
        "📝 سپس فایل زیرنویس را بفرست.\n"
        "▶️ ربات هاردساب را انجام می‌دهد و فایل نهایی را تحویل می‌دهد.\n\n"
        f"حداکثر حجم: {MAX_DOWNLOAD_MB}MB"
    )


async def media_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
    if not await ensure_admin(update):
        return

    msg = update.message

    # Ignore subtitle documents here; subtitle_handler handles them.
    if msg.document:
        ext = Path(msg.document.file_name or "").suffix.lower()
        if ext in SUB_EXTS:
            return

    if not (
        (msg.document or msg.video)
        or (msg.text and re.match(r"^https?://", msg.text.strip(), re.I))
    ):
        return

    old = JOBS.get(msg.chat_id)
    if old:
        await msg.reply_text(
            "⏳ یک کار قبلی هنوز در حال اجراست. لطفاً تا پایان آن صبر کن."
        )
        return

    workdir = job_dir(msg.chat_id)
    status = await msg.reply_text("⏳ دریافت ورودی...")

    try:
        video = await get_media_from_message(
            update, context, workdir, status
        )

        if not video:
            cleanup_job({"workdir": str(workdir)})
            return

        if video.suffix.lower() not in VIDEO_EXTS:
            # Allow unknown extension only if ffprobe recognizes it.
            try:
                await ffprobe_duration(video)
            except Exception:
                raise ValueError(
                    "فایل ارسال‌شده فرمت ویدئویی قابل تشخیص ندارد."
                )

        JOBS[msg.chat_id] = {
            "workdir": str(workdir),
            "video": str(video),
            "subtitle": None,
            "output": None,
        }

        await safe_edit(
            status,
            "✅ ویدئو دریافت شد.\n\n"
            "حالا فایل زیرنویس را بفرست.\n"
            "یا اگر ویدئو زیرنویس داخلی دارد، بنویس:\n"
            "/extract"
        )

    except Exception as e:
        log.exception("media_handler")
        await safe_edit(status, f"❌ خطا:\n{str(e)[-2500:]}")
        cleanup_job({"workdir": str(workdir)})
        JOBS.pop(msg.chat_id, None)


async def subtitle_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
    if not await ensure_admin(update):
        return

    msg = update.message

    if not msg.document:
        return

    ext = Path(msg.document.file_name or "").suffix.lower()
    if ext not in SUB_EXTS:
        return

    job = JOBS.get(msg.chat_id)

    if not job:
        await msg.reply_text(
            "⚠️ ابتدا ویدئو یا لینک ویدئو را ارسال کن."
        )
        return

    try:
        workdir = Path(job["workdir"])
        subtitle = workdir / safe_name(
            msg.document.file_name or "subtitle.srt",
            "subtitle.srt",
        )

        await msg.chat.send_action(ChatAction.TYPING)
        await download_telegram_file(msg, context, subtitle)

        job["subtitle"] = str(subtitle)

        await msg.reply_text(
            "✅ زیرنویس دریافت شد.\n"
            "در حال شروع هاردساب..."
        )

        video = Path(job["video"])
        await process_job(update, context, video, subtitle)

    except Exception as e:
        log.exception("subtitle_handler")
        await msg.reply_text(f"❌ خطا:\n{str(e)[-2500:]}")
        cleanup_job(job)
        JOBS.pop(msg.chat_id, None)


async def extract_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
    if not await ensure_admin(update):
        return

    job = JOBS.get(update.message.chat_id)

    if not job:
        await update.message.reply_text(
            "⚠️ ابتدا یک ویدئو یا لینک ویدئو ارسال کن."
        )
        return

    if job.get("subtitle"):
        await update.message.reply_text(
            "⚠️ برای این ویدئو قبلاً زیرنویس انتخاب شده است."
        )
        return

    try:
        video = Path(job["video"])
        subtitle = Path(job["workdir"]) / "internal_subtitle.srt"

        status = await update.message.reply_text(
            "🔎 در حال بررسی زیرنویس داخلی..."
        )

        await extract_internal_subtitle(video, subtitle)

        job["subtitle"] = str(subtitle)

        await safe_edit(
            status,
            "✅ زیرنویس داخلی استخراج شد.\n"
            "در حال شروع هاردساب..."
        )

        await process_job(update, context, video, subtitle)

    except Exception as e:
        log.exception("extract_command")
        await update.message.reply_text(
            f"❌ استخراج زیرنویس ناموفق بود:\n{str(e)[-2500:]}"
        )
        cleanup_job(job)
        JOBS.pop(update.message.chat_id, None)


async def cancel(update: Update, context: ContextTypes.DEFAULT_TYPE):
    if not await ensure_admin(update):
        return

    job = JOBS.pop(update.message.chat_id, None)

    if job:
        cleanup_job(job)
        LOCKS.pop(update.message.chat_id, None)
        await update.message.reply_text(
            "❌ عملیات لغو شد و فایل‌های موقت پاک شدند."
        )
    else:
        await update.message.reply_text("ℹ️ عملیات فعالی وجود ندارد.")


async def error_handler(update: object, context: ContextTypes.DEFAULT_TYPE):
    log.exception("Unhandled bot error", exc_info=context.error)


def main():
    log.info("Starting HardSub Bot...")
    log.info("Telegram Local Bot API: %s", BOT_API_BASE)

    app = (
        Application.builder()
        .token(BOT_TOKEN)
        .base_url(BOT_API_BASE)
        .base_file_url(BOT_FILE_BASE)
        .connect_timeout(30)
        .read_timeout(600)
        .write_timeout(600)
        .pool_timeout(30)
        .build()
    )

    app.add_handler(CommandHandler("start", start))
    app.add_handler(CommandHandler("extract", extract_command))
    app.add_handler(CommandHandler("cancel", cancel))

    # Subtitle first, then general media.
    app.add_handler(
        MessageHandler(filters.Document.ALL, subtitle_handler),
        group=0,
    )

    app.add_handler(
        MessageHandler(
            filters.Document.ALL | filters.VIDEO | filters.TEXT,
            media_handler,
        ),
        group=1,
    )

    app.add_error_handler(error_handler)

    log.info("Bot started")
    app.run_polling(
        allowed_updates=Update.ALL_TYPES,
        drop_pending_updates=False,
    )


if __name__ == "__main__":
    main()
