from __future__ import annotations

import html
import json
import os
import re
from dataclasses import dataclass, field
from typing import List, Optional, Tuple

from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder

MAX_SLUG_LEN = 50  # base slug; with -NN suffix the final id can reach 53. Callback budgets vs Telegram's 64-byte limit: "material:" 9+53=62, "mrm:" 4+53=57, "rm_yes:" 7+53=60, "watch:" 6+53=59.
DEFAULT_SLUG = "material"
DEFAULT_BUTTON_LABEL = "🔗 Смотреть запись"

# Human-readable Russian → ASCII map (not GOST/ISO; prefers readability).
# e.g. июнь → iyun (not iiun' as unidecode/GOST would give).
_RU_TRANSLIT = {
    "а": "a", "б": "b", "в": "v", "г": "g", "д": "d", "е": "e", "ё": "yo",
    "ж": "zh", "з": "z", "и": "i", "й": "y", "к": "k", "л": "l", "м": "m",
    "н": "n", "о": "o", "п": "p", "р": "r", "с": "s", "т": "t", "у": "u",
    "ф": "f", "х": "h", "ц": "c", "ч": "ch", "ш": "sh", "щ": "sch",
    "ъ": "", "ы": "y", "ь": "", "э": "e", "ю": "yu", "я": "ya",
}


@dataclass
class Material:
    id: str
    title: str
    description: str
    url: str
    button_label: str = DEFAULT_BUTTON_LABEL


@dataclass
class ArchiveWelcome:
    text: str
    photo_file_id: Optional[str] = None


@dataclass
class Archive:
    welcome: Optional[ArchiveWelcome] = None
    materials: List[Material] = field(default_factory=list)
    required_channel: Optional[str] = None


def _translit(text: str) -> str:
    """Russian → ASCII (human-readable). Non-Cyrillic chars are left unchanged here; the slugify regex strips them later."""
    out = []
    for ch in text.lower():
        out.append(_RU_TRANSLIT.get(ch, ch))
    return "".join(out)


def slugify(title: str, existing_ids: List[str]) -> str:
    """Transliterate `title` to ASCII kebab-case, cap base at 50 chars.
    With `-NN` suffix on duplicate, final id can reach 53 chars.
    Suffix `-2`, `-3`, ... as needed for uniqueness."""
    ascii_text = _translit(title)
    ascii_text = re.sub(r"[^a-z0-9]+", "-", ascii_text).strip("-")
    if not ascii_text:
        ascii_text = DEFAULT_SLUG
    base = ascii_text[:MAX_SLUG_LEN]
    if base not in existing_ids:
        return base
    n = 2
    while f"{base}-{n}" in existing_ids:
        n += 1
    return f"{base}-{n}"


def load_archive(path: str) -> Archive:
    """Load archive from JSON file. Missing file → empty Archive."""
    try:
        with open(path, encoding="utf-8") as f:
            data = json.load(f)
    except FileNotFoundError:
        return Archive(welcome=None, materials=[])

    welcome_data = data.get("welcome")
    welcome = None
    if welcome_data:
        welcome = ArchiveWelcome(
            text=welcome_data.get("text", ""),
            photo_file_id=welcome_data.get("photo_file_id") or None,
        )

    materials = [
        Material(
            id=m["id"],
            title=m["title"],
            description=m["description"],
            url=m["url"],
            button_label=m.get("button_label") or DEFAULT_BUTTON_LABEL,
        )
        for m in data.get("materials", [])
    ]
    required_channel = data.get("required_channel") or None
    return Archive(welcome=welcome, materials=materials, required_channel=required_channel)


def save_archive(path: str, archive: Archive) -> None:
    """Atomic write: tmp + rename."""
    payload = {
        "welcome": None
        if archive.welcome is None
        else {
            "text": archive.welcome.text,
            "photo_file_id": archive.welcome.photo_file_id,
        },
        "required_channel": archive.required_channel,
        "materials": [
            {
                "id": m.id,
                "title": m.title,
                "description": m.description,
                "url": m.url,
                "button_label": m.button_label,
            }
            for m in archive.materials
        ],
    }
    tmp = path + ".tmp"
    try:
        with open(tmp, "w", encoding="utf-8") as f:
            json.dump(payload, f, ensure_ascii=False, indent=2)
        os.replace(tmp, path)
    except OSError:
        try:
            os.unlink(tmp)
        except OSError:
            pass
        raise


def add_material(archive: Archive, title: str, description: str, url: str, button_label: str = "") -> Material:
    """Mutates `archive`. Generates slug-id; appends to materials. Returns the new Material.
    Empty button_label falls back to DEFAULT_BUTTON_LABEL."""
    existing_ids = [m.id for m in archive.materials]
    material_id = slugify(title, existing_ids)
    label = button_label.strip() or DEFAULT_BUTTON_LABEL
    mat = Material(id=material_id, title=title, description=description, url=url, button_label=label)
    archive.materials.append(mat)
    return mat


def remove_material(archive: Archive, material_id: str) -> bool:
    """Mutates `archive`. Returns True if found and removed, False otherwise."""
    for i, m in enumerate(archive.materials):
        if m.id == material_id:
            archive.materials.pop(i)
            return True
    return False


def set_welcome(archive: Archive, text: str, photo_file_id: Optional[str]) -> None:
    """Mutates `archive`. Replaces welcome."""
    archive.welcome = ArchiveWelcome(text=text, photo_file_id=photo_file_id)


def set_required_channel(archive: Archive, channel: Optional[str]) -> None:
    """Mutates `archive`. Sets the gate channel (None disables the gate)."""
    archive.required_channel = channel


def channel_url(channel: str) -> str:
    """@handle → https://t.me/handle."""
    return f"https://t.me/{channel.lstrip('@')}"


DEFAULT_ARCHIVE_WELCOME = (
    "👋 Сейчас активной конференции или вебинара нет.\n\n"
    "Ниже — записи прошлых мероприятий. Выбери, что интересно."
)


def build_archive_keyboard(materials: List[Material]) -> Optional[InlineKeyboardMarkup]:
    """One button per material. Returns None if list is empty."""
    if not materials:
        return None
    builder = InlineKeyboardBuilder()
    for m in materials:
        builder.row(InlineKeyboardButton(text=m.title, callback_data=f"material:{m.id}"))
    return builder.as_markup()


def format_material_response(material: Material, gated: bool) -> Tuple[str, InlineKeyboardMarkup]:
    """Returns (HTML text, keyboard) for a single material.
    gated=False → direct url button (opens immediately).
    gated=True  → callback button 'watch:<id>' (subscription checked on press)."""
    # title comes in raw from message.text → escape. description is stored as
    # message.html_text so Telegram formatting is preserved on purpose — leave it as-is.
    text = f"<b>{html.escape(material.title)}</b>\n\n{material.description}"
    builder = InlineKeyboardBuilder()
    if gated:
        builder.row(InlineKeyboardButton(text=material.button_label, callback_data=f"watch:{material.id}"))
    else:
        builder.row(InlineKeyboardButton(text=material.button_label, url=material.url))
    return text, builder.as_markup()


def format_list_materials(materials: List[Material]) -> str:
    if not materials:
        return "Материалов нет."
    lines = []
    for i, m in enumerate(materials, 1):
        lines.append(f"{i}. {m.title}")
        lines.append(f"   id: {m.id}")
        lines.append(f"   label: {m.button_label}")
        lines.append(f"   url: {m.url}")
    return "\n".join(lines)


def resolve_welcome_text(archive: Archive) -> str:
    """Returns welcome text from archive, or default if none/empty."""
    if archive.welcome and archive.welcome.text:
        return archive.welcome.text
    return DEFAULT_ARCHIVE_WELCOME
