"""
Shared helpers and in-memory runtime state.

This module must stay dependency-light (only `config` + python-telegram-bot)
so every other module can safely import from it without circular imports.
"""

from __future__ import annotations

import datetime as _dt
import html
import logging
import re
import time
from typing import Any, Optional
from urllib.parse import urlparse

from telegram import InlineKeyboardMarkup, Message
from telegram.constants import ParseMode

import config

logger = logging.getLogger(__name__)

# --------------------------------------------------------------------------- #
# In-memory runtime state (not persisted)
# --------------------------------------------------------------------------- #

# Download sessions keyed by random session token.
SESSIONS: dict[str, dict[str, Any]] = {}

# user_id -> token of the session with a running download.
USER_ACTIVE: dict[int, str] = {}

# user_id -> set of user ids currently extracting metadata.
USER_EXTRACTING: set[int] = set()

# user_id -> {"url": str, "ts": float} waiting for force-join verification.
PENDING_URL: dict[int, dict[str, Any]] = {}

# admin user id -> pending admin action name (a text input is expected).
ADMIN_PENDING: dict[int, str] = {}

# admin user id -> payload dict for the pending admin action.
PENDING_DATA: dict[int, dict[str, Any]] = {}

# Lifetimes for the cleanup job.
SESSION_TTL = 45 * 60          # seconds a finished session stays in memory
PENDING_URL_TTL = 30 * 60      # seconds a force-join pending URL stays valid


# --------------------------------------------------------------------------- #
# Small helpers
# --------------------------------------------------------------------------- #

def is_admin(user_id: Optional[int]) -> bool:
    """Return True when `user_id` equals the configured ADMIN_ID."""
    return config.ADMIN_ID is not None and user_id == config.ADMIN_ID


def now_str() -> str:
    """Current local timestamp as a sortable string (DB friendly)."""
    return time.strftime("%Y-%m-%d %H:%M:%S")


def past_date_str(days: int) -> str:
    """Timestamp `days` days ago as a sortable string."""
    return (_dt.datetime.now() - _dt.timedelta(days=days)).strftime("%Y-%m-%d %H:%M:%S")


# --------------------------------------------------------------------------- #
# URL validation
# --------------------------------------------------------------------------- #

_URL_RE = re.compile(r"^https?://[^\s]+$", re.IGNORECASE)


def is_valid_url(url: str) -> bool:
    """Structural URL validation (scheme + host). yt-dlp decides extractability."""
    if not url or len(url) > 2048 or " " in url:
        return False
    if not _URL_RE.match(url):
        return False
    try:
        parsed = urlparse(url)
    except ValueError:
        return False
    if parsed.scheme not in ("http", "https") or not parsed.netloc:
        return False
    return "." in parsed.netloc


# --------------------------------------------------------------------------- #
# Filename handling
# --------------------------------------------------------------------------- #

_ILLEGAL_FILENAME_CHARS = re.compile(r'[<>:"/\\|?*\x00-\x1f]')


def sanitize_filename(name: str, max_length: int = 60) -> str:
    """Make a website-supplied string safe as a file name on Windows/Linux.

    Removes illegal characters, control characters, trims trailing dots and
    spaces (illegal on Windows) and caps the length while staying Unicode-safe.
    """
    name = html.unescape(name or "")
    name = _ILLEGAL_FILENAME_CHARS.sub("_", name)
    name = re.sub(r"\s+", " ", name).strip().strip(" .")
    if len(name) > max_length:
        name = name[:max_length].rstrip(" .")
    return name or "video"


# --------------------------------------------------------------------------- #
# Formatting helpers
# --------------------------------------------------------------------------- #

def escape_html(text: Optional[str]) -> str:
    """Escape text for safe inclusion in HTML parse-mode messages."""
    return html.escape(str(text if text is not None else ""), quote=False)


def format_size(num_bytes: Optional[float]) -> str:
    """Human-readable byte size (Persian 'unknown' placeholder when missing)."""
    if not num_bytes or num_bytes <= 0:
        return "نامشخص"
    units = ["B", "KB", "MB", "GB", "TB"]
    value = float(num_bytes)
    for unit in units:
        if value < 1024 or unit == units[-1]:
            return f"{int(value)} {unit}" if unit == "B" else f"{value:.1f} {unit}"
        value /= 1024
    return "نامشخص"


def format_duration(seconds: Optional[float]) -> str:
    """Seconds -> MM:SS or HH:MM:SS."""
    if seconds is None or seconds < 0:
        return "نامشخص"
    total = int(seconds)
    hours, remainder = divmod(total, 3600)
    minutes, secs = divmod(remainder, 60)
    if hours:
        return f"{hours:02d}:{minutes:02d}:{secs:02d}"
    return f"{minutes:02d}:{secs:02d}"


def progress_bar(percent: float, width: int = 12) -> str:
    """Unicode block progress bar."""
    percent = max(0.0, min(100.0, percent))
    filled = int(round(percent / 100.0 * width))
    return "█" * filled + "░" * (width - filled)


# --------------------------------------------------------------------------- #
# Telegram message helper
# --------------------------------------------------------------------------- #

async def safe_edit(
    message: Optional[Message],
    text: str,
    reply_markup: Optional[InlineKeyboardMarkup] = None,
) -> None:
    """Edit a message; silently ignore 'not modified' / deleted-message errors."""
    if message is None:
        return
    try:
        await message.edit_text(text, parse_mode=ParseMode.HTML, reply_markup=reply_markup)
    except Exception as exc:  # noqa: BLE001 - any edit failure is non-fatal
        logger.debug("safe_edit failed: %s", exc)
