from __future__ import annotations

import argparse
import html
import io
import json
import mimetypes
import os
import re
import ssl
import subprocess
import sys
import threading
import zipfile
from dataclasses import dataclass, field
from http import HTTPStatus
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from typing import Any
from urllib.parse import parse_qs, urlencode, urlparse


IMAGE_EXTENSIONS = {
    ".jpg",
    ".jpeg",
    ".png",
    ".gif",
    ".webp",
    ".bmp",
    ".tif",
    ".tiff",
}
MANUSCRIPT_RE = re.compile(r"(?i)\bBJ(?:[- _][IVXLCDM]+)?[- _]?(\d{1,4})")
PAGE_RE = re.compile(r"(\d+)\s*([vV])?")
COPY_SUFFIX_RE = re.compile(r"(?i)\s+copy$")
DEFAULT_PORT = 8000
DEFAULT_HTTPS_PORT = 8443
DEFAULT_ROOT = Path(
    r"D:\manuscrise bolyai ianos"
)
FIREWALL_RULE_PREFIX = "MS_BJ Browser"
TRANSKRIBUS_URL = "https://www.transkribus.org/"
OFFICIAL_THEMATIC_CATEGORIES = [
    "I. documente",
    "II. scrisori si ciorne de scrisori",
    "III. autobiografie si scrieri despre tata",
    "IV. proiecte de pagini de titlu",
    "V. prefete",
    "VI. despre doctrina",
    "VII. doctrina conducatoare",
    "VIII. despre publicarea doctrinei",
    "IX. impartirea Pamantului in 12 parti si academie mondiala",
    "X. despre mantuire bine frumos adevar",
    "XI. istoria stiintei",
    "XII. demografie si populatie",
    "XIII. filozofie",
    "XIV. societatea trecut prezent viitor",
    "XV. etica si drept",
    "XVI. psihologie",
    "XVII. teoria monedei si case de economii",
    "XVIII. lingvistica",
    "XIX. industrie si mestesuguri",
    "XX. cultura si educatie",
    "XXI. teoria lampilor si a sobelor",
    "XXII. medicina si terapeutica",
    "XXIII. despre arte",
    "XXIV. muzica",
    "XXV. matematica",
    "XXVI. manuscrise diverse",
]
SECTION_CATEGORY_MAP = {
    category.split(".", 1)[0].upper(): category
    for category in OFFICIAL_THEMATIC_CATEGORIES
}


@dataclass(slots=True)
class RootSpec:
    label: str
    path: Path


@dataclass(slots=True)
class ImageEntry:
    absolute_path: Path
    archive_label: str
    relative_path: str
    folder_key: str
    folder_name: str
    filename: str
    manuscript_number: int | None
    page_label: str
    sort_key: tuple[int, int, str]

    @property
    def image_url(self) -> str:
        return f"/image?{urlencode({'path': self.resource_key})}"

    @property
    def resource_key(self) -> str:
        return build_resource_key(self.archive_label, self.relative_path)

    @property
    def viewer_url(self) -> str:
        return f"/viewer?{urlencode({'path': self.resource_key})}"


@dataclass(slots=True)
class FolderRecord:
    key: str
    name: str
    archive_label: str
    absolute_path: Path
    relative_path: str
    section_code: str = ""
    thematic_category: str = ""
    images: list[ImageEntry] = field(default_factory=list)
    images_by_manuscript: dict[int, list[ImageEntry]] = field(default_factory=dict)
    unmatched_images: list[ImageEntry] = field(default_factory=list)
    display_title: str = ""
    notes: str = ""

    @property
    def manuscripts(self) -> list[int]:
        return sorted(self.images_by_manuscript)

    @property
    def image_count(self) -> int:
        return len(self.images)

    @property
    def unmatched_count(self) -> int:
        return len(self.unmatched_images)

    @property
    def first_manuscript(self) -> int | None:
        values = self.manuscripts
        return values[0] if values else None

    @property
    def last_manuscript(self) -> int | None:
        values = self.manuscripts
        return values[-1] if values else None

    @property
    def effective_title(self) -> str:
        return self.display_title or self.name


@dataclass(slots=True)
class ManuscriptRecord:
    number: int
    images: list[ImageEntry] = field(default_factory=list)
    folders: set[str] = field(default_factory=set)
    title: str = ""
    notes: str = ""
    manual_categories: list[str] = field(default_factory=list)
    auto_categories: list[str] = field(default_factory=list)

    @property
    def effective_title(self) -> str:
        return self.title or f"Manuscris BJ-{self.number}"

    @property
    def categories(self) -> list[str]:
        return merge_categories(self.auto_categories, self.manual_categories)

    @property
    def editable_categories(self) -> list[str]:
        return list(self.manual_categories)


@dataclass(slots=True)
class CategoryRecord:
    name: str
    manuscripts: list[int] = field(default_factory=list)

    @property
    def count(self) -> int:
        return len(self.manuscripts)


@dataclass(slots=True)
class IndexSnapshot:
    root: Path
    roots: list[RootSpec]
    folders: dict[str, FolderRecord]
    manuscripts: dict[int, ManuscriptRecord]
    categories: dict[str, CategoryRecord]
    total_images: int
    unmatched_images: int
    scanned_at: str


class LibraryIndex:
    def __init__(self, root: Path, metadata_path: Path):
        self.root = root.resolve()
        self.metadata_path = metadata_path
        self._lock = threading.RLock()
        self._snapshot: IndexSnapshot | None = None
        self._roots_by_label: dict[str, Path] = {}
        self._images_by_resource_key: dict[str, ImageEntry] = {}
        self.refresh()

    def refresh(self) -> None:
        with self._lock:
            metadata = self._load_metadata()
            folders: dict[str, FolderRecord] = {}
            manuscripts: dict[int, ManuscriptRecord] = {}
            categories = normalize_categories(metadata.get("categories", []))
            total_images = 0
            unmatched_images = 0
            images_by_resource_key: dict[str, ImageEntry] = {}

            roots = discover_roots(self.root)
            self._roots_by_label = {spec.label: spec.path for spec in roots}

            for root_spec in roots:
                for folder_path in sorted(
                    [item for item in root_spec.path.iterdir() if item.is_dir()],
                    key=lambda item: item.name.lower(),
                ):
                    folder_key = build_folder_key(root_spec.label, folder_path.name)
                    folder = FolderRecord(
                        key=folder_key,
                        name=folder_path.name,
                        archive_label=root_spec.label,
                        absolute_path=folder_path,
                        relative_path=str(folder_path.relative_to(root_spec.path)),
                        section_code=extract_folder_section_code(folder_path.name) or "",
                        thematic_category=infer_folder_thematic_category(folder_path.name),
                        display_title=metadata["folders"].get(folder_key, {}).get(
                            "display_title", ""
                        ),
                        notes=metadata["folders"].get(folder_key, {}).get(
                            "notes", ""
                        ),
                    )

                    for path in sorted(
                        folder_path.rglob("*"), key=lambda item: str(item).lower()
                    ):
                        if not path.is_file():
                            continue
                        if path.suffix.lower() not in IMAGE_EXTENSIONS:
                            continue

                        entry = build_image_entry(
                            root_spec, folder_key, folder_path.name, path
                        )
                        images_by_resource_key[entry.resource_key] = entry
                        folder.images.append(entry)
                        total_images += 1

                        if entry.manuscript_number is None:
                            folder.unmatched_images.append(entry)
                            unmatched_images += 1
                            continue

                        bucket = folder.images_by_manuscript.setdefault(
                            entry.manuscript_number, []
                        )
                        bucket.append(entry)

                        manuscript = manuscripts.setdefault(
                            entry.manuscript_number, ManuscriptRecord(entry.manuscript_number)
                        )
                        manuscript.images.append(entry)
                        manuscript.folders.add(folder.key)

                    folder.images.sort(
                        key=lambda item: (item.manuscript_number or 10**9, *item.sort_key)
                    )
                    folder.unmatched_images.sort(key=lambda item: item.filename.lower())
                    for items in folder.images_by_manuscript.values():
                        items.sort(key=lambda item: item.sort_key)
                    folders[folder.key] = folder

            for number, manuscript in manuscripts.items():
                manuscript.images.sort(
                    key=lambda item: (
                        item.manuscript_number or 10**9,
                        item.folder_name.lower(),
                        item.archive_label.lower(),
                        item.sort_key,
                    )
                )
                meta = metadata["manuscripts"].get(str(number), {})
                manuscript.title = meta.get("title", "")
                manuscript.notes = meta.get("notes", "")
                manuscript.manual_categories = normalize_categories(meta.get("categories", []))
                auto_categories: list[str] = []
                for folder_key in sorted(manuscript.folders):
                    folder_category = folders.get(folder_key).thematic_category if folder_key in folders else ""
                    if folder_category:
                        auto_categories = merge_categories(auto_categories, [folder_category])
                manuscript.auto_categories = auto_categories
                categories = merge_categories(categories, manuscript.categories)

            category_records = {
                category: CategoryRecord(category) for category in categories
            }
            for manuscript in manuscripts.values():
                for category in manuscript.categories:
                    category_records.setdefault(
                        category, CategoryRecord(category)
                    ).manuscripts.append(manuscript.number)
            for record in category_records.values():
                record.manuscripts.sort()

            self._snapshot = IndexSnapshot(
                root=self.root,
                roots=roots,
                folders=folders,
                manuscripts=dict(sorted(manuscripts.items())),
                categories=dict(
                    sorted(
                        category_records.items(),
                        key=lambda item: category_sort_key(item[0]),
                    )
                ),
                total_images=total_images,
                unmatched_images=unmatched_images,
                scanned_at=self._format_timestamp(),
            )
            self._images_by_resource_key = images_by_resource_key

    def get_snapshot(self) -> IndexSnapshot:
        with self._lock:
            if self._snapshot is None:
                raise RuntimeError("Indexul nu a fost initializat.")
            return self._snapshot

    def get_folder(self, key: str) -> FolderRecord | None:
        return self.get_snapshot().folders.get(key)

    def get_manuscript(self, number: int) -> ManuscriptRecord | None:
        return self.get_snapshot().manuscripts.get(number)

    def get_image(self, resource_key: str) -> ImageEntry | None:
        with self._lock:
            return self._images_by_resource_key.get(resource_key)

    def get_viewer_navigation(
        self, image: ImageEntry
    ) -> tuple[ImageEntry | None, ImageEntry | None, str]:
        snapshot = self.get_snapshot()
        sequence: list[ImageEntry] = []
        scope_label = "Navigare in folder"

        if image.manuscript_number is not None:
            manuscript = snapshot.manuscripts.get(image.manuscript_number)
            if manuscript:
                sequence = manuscript.images
                scope_label = f"Navigare in cota BJ-{image.manuscript_number}"

        if not sequence:
            folder = snapshot.folders.get(image.folder_key)
            if folder:
                if image.manuscript_number is None:
                    sequence = folder.unmatched_images
                    scope_label = f"Navigare in imaginile neclare din folderul {folder.name}"
                else:
                    sequence = folder.images_by_manuscript.get(image.manuscript_number, [])
                    scope_label = f"Navigare in folderul {folder.name}"

        resource_key = image.resource_key
        previous_image: ImageEntry | None = None
        next_image: ImageEntry | None = None
        for index, candidate in enumerate(sequence):
            if candidate.resource_key != resource_key:
                continue
            if index > 0:
                previous_image = sequence[index - 1]
            if index + 1 < len(sequence):
                next_image = sequence[index + 1]
            break
        return previous_image, next_image, scope_label

    def resolve_relative_path(self, resource_key: str) -> Path | None:
        archive_label, relative_path = parse_resource_key(resource_key)
        if not archive_label or archive_label not in self._roots_by_label:
            return None
        candidate = (self._roots_by_label[archive_label] / relative_path).resolve()
        try:
            candidate.relative_to(self._roots_by_label[archive_label])
        except ValueError:
            return None
        return candidate if candidate.is_file() else None

    def find_folders_by_name(self, name: str) -> list[FolderRecord]:
        snapshot = self.get_snapshot()
        return sorted(
            [folder for folder in snapshot.folders.values() if folder.name == name],
            key=lambda folder: (folder.archive_label.lower(), folder.name.lower()),
        )

    def save_folder_metadata(self, key: str, display_title: str, notes: str) -> bool:
        with self._lock:
            metadata = self._load_metadata()
            payload = {
                "display_title": display_title.strip(),
                "notes": notes.strip(),
            }
            if payload["display_title"] or payload["notes"]:
                metadata["folders"][key] = payload
            else:
                metadata["folders"].pop(key, None)
            self._write_metadata(metadata)
            self.refresh()
            return True

    def save_manuscript_metadata(
        self,
        number: int,
        title: str,
        notes: str,
        categories_raw: Any = "",
    ) -> bool:
        with self._lock:
            metadata = self._load_metadata()
            categories = normalize_categories(categories_raw)
            payload = {
                "title": title.strip(),
                "notes": notes.strip(),
                "categories": categories,
            }
            if payload["title"] or payload["notes"] or payload["categories"]:
                metadata["manuscripts"][str(number)] = payload
            else:
                metadata["manuscripts"].pop(str(number), None)
            metadata["categories"] = merge_categories(
                metadata.get("categories", []),
                self._categories_used_in_metadata(metadata),
            )
            self._write_metadata(metadata)
            self.refresh()
            return True

    def save_categories(self, categories_raw: Any) -> bool:
        with self._lock:
            metadata = self._load_metadata()
            metadata["categories"] = merge_categories(
                normalize_categories(categories_raw),
                self._categories_used_in_metadata(metadata),
            )
            self._write_metadata(metadata)
            self.refresh()
            return True

    def add_category(self, category_raw: Any) -> bool:
        categories = normalize_categories(category_raw)
        if not categories:
            return False
        with self._lock:
            metadata = self._load_metadata()
            metadata["categories"] = merge_categories(
                metadata.get("categories", []),
                categories,
                self._categories_used_in_metadata(metadata),
            )
            self._write_metadata(metadata)
            self.refresh()
            return True

    def _load_metadata(self) -> dict[str, Any]:
        if not self.metadata_path.exists():
            return {"folders": {}, "manuscripts": {}, "categories": []}
        try:
            data = json.loads(self.metadata_path.read_text(encoding="utf-8"))
        except json.JSONDecodeError:
            return {"folders": {}, "manuscripts": {}, "categories": []}
        if not isinstance(data, dict):
            return {"folders": {}, "manuscripts": {}, "categories": []}
        return {
            "folders": dict(data.get("folders", {})),
            "manuscripts": dict(data.get("manuscripts", {})),
            "categories": normalize_categories(data.get("categories", [])),
        }

    @staticmethod
    def _categories_used_in_metadata(metadata: dict[str, Any]) -> list[str]:
        categories: list[str] = []
        manuscripts = metadata.get("manuscripts", {})
        if not isinstance(manuscripts, dict):
            return categories
        for item in manuscripts.values():
            if isinstance(item, dict):
                categories = merge_categories(categories, item.get("categories", []))
        return categories

    def _write_metadata(self, payload: dict[str, Any]) -> None:
        self.metadata_path.parent.mkdir(parents=True, exist_ok=True)
        self.metadata_path.write_text(
            json.dumps(payload, ensure_ascii=False, indent=2),
            encoding="utf-8",
        )

    @staticmethod
    def _format_timestamp() -> str:
        from datetime import datetime

        return datetime.now().strftime("%Y-%m-%d %H:%M:%S")


def clean_base_name(filename: str) -> str:
    base = Path(filename).stem
    return COPY_SUFFIX_RE.sub("", base).strip()


def archive_short_label(label: str) -> str:
    parts = label.split("-")
    return "-".join(parts[-2:]) if len(parts) >= 2 else label


def build_folder_key(archive_label: str, folder_name: str) -> str:
    return f"{archive_label}::{folder_name}"


def build_resource_key(archive_label: str, relative_path: str) -> str:
    normalized = relative_path.replace("\\", "/")
    return f"{archive_label}::{normalized}"


def parse_resource_key(resource_key: str) -> tuple[str | None, str]:
    if "::" not in resource_key:
        return None, resource_key
    archive_label, relative_path = resource_key.split("::", 1)
    return archive_label, relative_path.replace("/", os.sep)


def safe_download_name(value: str) -> str:
    cleaned = re.sub(r"[^A-Za-z0-9._-]+", "_", str(value or "")).strip("._")
    return cleaned or "download"


def default_metadata_path() -> Path:
    return Path(__file__).resolve().parent / "data" / "metadata.json"


def validate_port(port: int) -> int:
    try:
        value = int(port)
    except (TypeError, ValueError) as exc:
        raise ValueError(
            "Port invalid. Foloseste un numar intre 1 si 65535."
        ) from exc
    if not 1 <= value <= 65535:
        raise ValueError("Port invalid. Foloseste un numar intre 1 si 65535.")
    return value


def validate_tls_files(
    certfile: Path | None,
    keyfile: Path | None,
) -> tuple[Path | None, Path | None]:
    if certfile is None and keyfile is None:
        return None, None
    if certfile is None:
        raise ValueError(
            "Pentru HTTPS trebuie specificat cel putin fisierul certificatului."
        )

    resolved_certfile = Path(certfile).expanduser().resolve()
    if not resolved_certfile.exists() or not resolved_certfile.is_file():
        raise ValueError(f"Fisier certificat invalid: {resolved_certfile}")

    resolved_keyfile: Path | None = None
    if keyfile is not None:
        resolved_keyfile = Path(keyfile).expanduser().resolve()
        if not resolved_keyfile.exists() or not resolved_keyfile.is_file():
            raise ValueError(f"Fisier cheie privata invalid: {resolved_keyfile}")
    return resolved_certfile, resolved_keyfile


def build_local_url(scheme: str, port: int) -> str:
    return f"{scheme}://127.0.0.1:{port}"


def build_bound_url(scheme: str, host: str, port: int) -> str:
    return f"{scheme}://{host}:{port}"


def should_configure_firewall(host: str, enabled: bool) -> bool:
    if not enabled:
        return False
    return host.strip().lower() not in {"127.0.0.1", "localhost", "::1"}


def firewall_rule_name(port: int) -> str:
    return f"{FIREWALL_RULE_PREFIX} {validate_port(port)}/TCP"


def is_windows_admin() -> bool:
    if os.name != "nt":
        return False
    try:
        import ctypes

        return bool(ctypes.windll.shell32.IsUserAnAdmin())
    except Exception:
        return False


def ensure_windows_firewall_rule(
    port: int,
    rule_name: str | None = None,
) -> tuple[bool, str]:
    validated_port = validate_port(port)
    if os.name != "nt":
        return True, "Configurarea automata a firewall-ului este disponibila doar pe Windows."
    if not is_windows_admin():
        return (
            False,
            "Firewall-ul Windows nu a putut fi configurat automat. "
            "Porneste scriptul ca Administrator pentru a deschide portul in Windows Firewall.",
        )

    resolved_rule_name = rule_name or firewall_rule_name(validated_port)
    common_args = [
        "protocol=TCP",
        f"localport={validated_port}",
    ]
    subprocess.run(
        [
            "netsh",
            "advfirewall",
            "firewall",
            "delete",
            "rule",
            f"name={resolved_rule_name}",
            *common_args,
        ],
        capture_output=True,
        text=True,
        encoding="utf-8",
        errors="replace",
        check=False,
    )
    result = subprocess.run(
        [
            "netsh",
            "advfirewall",
            "firewall",
            "add",
            "rule",
            f"name={resolved_rule_name}",
            "dir=in",
            "action=allow",
            *common_args,
            "profile=any",
            "enable=yes",
        ],
        capture_output=True,
        text=True,
        encoding="utf-8",
        errors="replace",
        check=False,
    )
    if result.returncode == 0:
        return (
            True,
            f"Firewall Windows configurat: portul {validated_port}/TCP este permis pentru acces extern.",
        )

    details = (result.stderr or result.stdout).strip()
    suffix = f" Detalii: {details}" if details else ""
    return (
        False,
        f"Firewall-ul Windows nu a putut fi configurat automat pentru portul {validated_port}/TCP.{suffix}",
    )


def discover_roots(base_path: Path) -> list[RootSpec]:
    base_path = base_path.resolve()
    matches = []
    for candidate in sorted(base_path.glob("MS_BJ-20260520T083533Z-*")):
        root = candidate / "MS_BJ"
        if root.is_dir():
            matches.append(RootSpec(candidate.name, root))
    if matches:
        return matches
    if base_path.name == "MS_BJ" and base_path.is_dir():
        return [RootSpec(base_path.parent.name, base_path)]
    if (base_path / "MS_BJ").is_dir():
        return [RootSpec(base_path.name, base_path / "MS_BJ")]
    if base_path.is_dir():
        return [RootSpec(base_path.name, base_path)]
    return []


def validate_root(root: Path) -> Path:
    candidate = Path(root).expanduser().resolve()
    if not candidate.exists() or not candidate.is_dir():
        raise ValueError(f"Director invalid: {candidate}")
    if not discover_roots(candidate):
        raise ValueError(
            f"Nu am gasit directoare sursa indexabile sub: {candidate}"
        )
    return candidate


def extract_manuscript_number(filename: str) -> tuple[int | None, re.Match[str] | None]:
    match = MANUSCRIPT_RE.search(clean_base_name(filename))
    if not match:
        return None, None
    return int(match.group(1)), match


def extract_page_label(filename: str, manuscript_number: int | None, match: re.Match[str] | None) -> str:
    base = clean_base_name(filename)
    if manuscript_number is None or match is None:
        return ""

    tail = base[match.end() :]
    tail = re.sub(rf"(?i)\b{manuscript_number}[_-]", "", tail)
    tokens = []
    for page_number, verso in PAGE_RE.findall(tail):
        token = f"{int(page_number)}{'v' if verso else ''}"
        tokens.append(token)
    return ", ".join(tokens)


def page_sort_key(filename: str, page_label: str) -> tuple[int, int, str]:
    match = PAGE_RE.search(page_label)
    if not match:
        return (10**9, 0, filename.lower())
    page_number = int(match.group(1))
    verso_rank = 1 if match.group(2) else 0
    return (page_number, verso_rank, filename.lower())


def build_image_entry(
    root_spec: RootSpec,
    folder_key: str,
    folder_name: str,
    path: Path,
) -> ImageEntry:
    manuscript_number, match = extract_manuscript_number(path.name)
    label = extract_page_label(path.name, manuscript_number, match)
    return ImageEntry(
        absolute_path=path,
        archive_label=root_spec.label,
        relative_path=str(path.relative_to(root_spec.path)),
        folder_key=folder_key,
        folder_name=folder_name,
        filename=path.name,
        manuscript_number=manuscript_number,
        page_label=label,
        sort_key=page_sort_key(path.name, label),
    )


def compact_manuscript_label(numbers: list[int]) -> str:
    if not numbers:
        return "fara manuscrise detectate"
    if len(numbers) <= 6:
        return ", ".join(f"BJ-{number}" for number in numbers)
    return f"BJ-{numbers[0]} ... BJ-{numbers[-1]} ({len(numbers)} detectate)"


def normalize_category_name(value: Any) -> str:
    return re.sub(r"\s+", " ", str(value or "")).strip(" \t\r\n,;")


def parse_roman_numeral(value: str) -> int | None:
    if not value or not re.fullmatch(r"[IVXLCDM]+", value.upper()):
        return None

    roman_values = {
        "I": 1,
        "V": 5,
        "X": 10,
        "L": 50,
        "C": 100,
        "D": 500,
        "M": 1000,
    }
    total = 0
    previous = 0
    for character in reversed(value.upper()):
        current = roman_values[character]
        if current < previous:
            total -= current
        else:
            total += current
            previous = current
    return total


def category_sort_key(category: str) -> tuple[int, int, str]:
    normalized = normalize_category_name(category)
    match = re.match(r"^([IVXLCDM]+)\.\s+", normalized, re.IGNORECASE)
    if match:
        numeral_value = parse_roman_numeral(match.group(1))
        if numeral_value is not None:
            return (0, numeral_value, normalized.casefold())
    return (1, 10**9, normalized.casefold())


def extract_folder_section_code(folder_name: str) -> str | None:
    match = re.match(r"(?i)^BJ\s+([IVXLCDM]+)\b", normalize_category_name(folder_name))
    if not match:
        return None
    return match.group(1).upper()


def infer_folder_thematic_category(folder_name: str) -> str:
    section_code = extract_folder_section_code(folder_name)
    if not section_code:
        return ""
    return SECTION_CATEGORY_MAP.get(section_code, "")


def normalize_categories(raw: Any) -> list[str]:
    items: list[str] = []
    if isinstance(raw, str):
        items = re.split(r"[\n,;]+", raw)
    elif isinstance(raw, (list, tuple, set)):
        for item in raw:
            if isinstance(item, str):
                items.extend(re.split(r"[\n,;]+", item))
            else:
                items.append(str(item))
    elif raw:
        items = [str(raw)]

    result: list[str] = []
    seen: set[str] = set()
    for item in items:
        normalized = normalize_category_name(item)
        if not normalized:
            continue
        key = normalized.casefold()
        if key in seen:
            continue
        seen.add(key)
        result.append(normalized)
    return result


def merge_categories(*groups: Any) -> list[str]:
    merged: list[str] = []
    seen: set[str] = set()
    for group in groups:
        for item in normalize_categories(group):
            key = item.casefold()
            if key in seen:
                continue
            seen.add(key)
            merged.append(item)
    return merged


def render_category_badges(categories: list[str], linked: bool = True) -> str:
    if not categories:
        return "<span class='muted'>fara categorie</span>"
    badges = []
    for category in categories:
        if linked:
            badges.append(
                f"<a class='badge-link' href='/?{urlencode({'category': category})}'>{escape(category)}</a>"
            )
        else:
            badges.append(f"<span class='badge-chip'>{escape(category)}</span>")
    return "".join(badges)


def render_category_datalist(datalist_id: str, categories: list[str]) -> str:
    if not categories:
        return ""
    options = "".join(
        f"<option value='{escape(category)}'></option>" for category in categories
    )
    return f"<datalist id='{escape(datalist_id)}'>{options}</datalist>"


def escape(value: Any) -> str:
    return html.escape("" if value is None else str(value), quote=True)


def paragraphize(text: str) -> str:
    if not text.strip():
        return "<p class='muted'>Nu exista notite salvate.</p>"
    paragraphs = []
    for block in text.strip().splitlines():
        block = block.strip()
        if block:
            paragraphs.append(f"<p>{escape(block)}</p>")
    return "".join(paragraphs)


def build_status_message(status: str) -> str:
    mapping = {
        "refreshed": "Indexul a fost reimprospatat.",
        "folder-saved": "Modificarile folderului au fost salvate.",
        "manuscript-saved": "Modificarile manuscrisului au fost salvate.",
        "categories-saved": "Lista de categorii a fost salvata.",
        "category-added": "Categoria noua a fost adaugata.",
        "folder-opened": "Folderul a fost trimis catre Explorer.",
        "invalid-folder": "Folderul cerut nu exista.",
        "invalid-category": "Introdu o categorie noua inainte de salvare.",
        "invalid-manuscript": "Manuscrisul cerut nu exista.",
        "invalid-image": "Imaginea ceruta nu a fost gasita.",
    }
    message = mapping.get(status)
    if not message:
        return ""
    return f"<div class='flash'>{escape(message)}</div>"


def nav(active: str) -> str:
    items = [
        ("manuscripts", "/", "Manuscrise"),
        ("folders", "/folders", "Foldere"),
        ("categories", "/categories", "Categorii"),
        ("unmatched", "/unmatched", "Neclare"),
    ]
    links = []
    for key, href, label in items:
        class_name = "nav-link active" if key == active else "nav-link"
        links.append(f"<a class='{class_name}' href='{href}'>{escape(label)}</a>")
    return "".join(links)


def layout(title: str, active: str, content: str, extra_head: str = "") -> bytes:
    document = f"""<!doctype html>
<html lang="ro">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>{escape(title)}</title>
  <link rel="stylesheet" href="/static/style.css">
  {extra_head}
</head>
<body>
  <div class="page-shell">
    <header class="topbar">
      <div class="brand">
        <div class="brand-mark">BJ</div>
        <div>
          <h1>Arhiva manuscriselor</h1>
          <p>Index local, ordonat dupa numarul manuscrisului</p>
        </div>
      </div>
      <nav class="main-nav">{nav(active)}</nav>
    </header>
    <main>{content}</main>
  </div>
  <script>
    document.querySelectorAll('[data-filter-input]').forEach((input) => {{
      const targetId = input.getAttribute('data-filter-input');
      const target = document.getElementById(targetId);
      if (!target) return;
      const items = [...target.querySelectorAll('[data-search]')];
      input.addEventListener('input', () => {{
        const query = input.value.trim().toLowerCase();
        items.forEach((item) => {{
          const haystack = item.getAttribute('data-search') || '';
          item.style.display = !query || haystack.includes(query) ? '' : 'none';
        }});
      }});
    }});
  </script>
</body>
</html>
"""
    return document.encode("utf-8")


def render_summary(snapshot: IndexSnapshot) -> str:
    folder_count = len(snapshot.folders)
    manuscript_count = len(snapshot.manuscripts)
    category_count = len(snapshot.categories)
    return f"""
<section class="hero">
  <div class="hero-copy">
    <p class="eyebrow">Baza de lucru</p>
    <h2>{escape(snapshot.root.name)}</h2>
    <p class="detail-path">{escape(str(snapshot.root))}</p>
    <p>Scanare facuta la {escape(snapshot.scanned_at)}. Interfata combina toate pachetele detectate si afiseaza manuscrisele in ordine, folderele sursa si fisierele neclare separat.</p>
  </div>
  <div class="hero-stats">
    <div class="stat"><span>{len(snapshot.roots)}</span><small>pachete</small></div>
    <div class="stat"><span>{manuscript_count}</span><small>manuscrise</small></div>
    <div class="stat"><span>{category_count}</span><small>categorii</small></div>
    <div class="stat"><span>{folder_count}</span><small>foldere</small></div>
    <div class="stat"><span>{snapshot.total_images}</span><small>imagini</small></div>
    <div class="stat"><span>{snapshot.unmatched_images}</span><small>neclare</small></div>
  </div>
</section>
"""


def render_manuscripts_page(
    snapshot: IndexSnapshot, status: str, selected_category: str = ""
) -> bytes:
    if selected_category and selected_category not in snapshot.categories:
        selected_category = ""

    rows = []
    manuscripts = [
        manuscript
        for manuscript in snapshot.manuscripts.values()
        if not selected_category or selected_category in manuscript.categories
    ]
    category_options = ["<option value=''>Toate categoriile</option>"]
    for category in snapshot.categories:
        selected = " selected" if category == selected_category else ""
        category_options.append(
            f"<option value='{escape(category)}'{selected}>{escape(category)}</option>"
        )
    selected_hint = (
        f"<p class='muted'>Filtru activ: <strong>{escape(selected_category)}</strong></p>"
        if selected_category
        else "<p class='muted'>Poti combina cautarea libera cu filtrarea dupa categorii.</p>"
    )
    for manuscript in manuscripts:
        folder_links = []
        folder_search_bits = []
        for key in sorted(manuscript.folders):
            folder = snapshot.folders[key]
            label = f"{folder.name} | {archive_short_label(folder.archive_label)}"
            folder_search_bits.append(label)
            folder_links.append(
                f"<a class='mini-link' href='/folder?{urlencode({'key': folder.key})}'>{escape(label)}</a>"
            )
        notes = escape(manuscript.notes[:90] + ("..." if len(manuscript.notes) > 90 else ""))
        category_badges = render_category_badges(manuscript.categories)
        search_blob = " ".join(
            [
                str(manuscript.number),
                manuscript.title,
                manuscript.notes,
                " ".join(folder_search_bits),
                " ".join(manuscript.categories),
            ]
        ).lower()
        rows.append(
            f"""
<tr data-search="{escape(search_blob)}">
  <td><a class="strong-link" href="/manuscript?{urlencode({'number': manuscript.number})}">BJ-{manuscript.number}</a></td>
  <td>{escape(manuscript.title or 'fara titlu')}</td>
  <td>{category_badges}</td>
  <td>{len(manuscript.images)}</td>
  <td>{len(manuscript.folders)}</td>
  <td>{" ".join(folder_links) or '<span class="muted">-</span>'}</td>
  <td>{notes or '<span class="muted">-</span>'}</td>
</tr>
"""
        )

    content = f"""
{build_status_message(status)}
{render_summary(snapshot)}
<section class="panel">
  <div class="panel-head">
    <div>
      <p class="eyebrow">Navigare principala</p>
      <h3>Lista manuscriselor</h3>
      {selected_hint}
    </div>
    <form method="post" action="/refresh">
      <input type="hidden" name="next" value="/">
      <button class="button button-secondary" type="submit">Refresh index</button>
    </form>
  </div>
  <div class="toolbar">
    <input class="search-input" data-filter-input="manuscripts-table" type="search" placeholder="Cauta dupa numar, folder, titlu sau notite">
    <form class="toolbar-form" method="get" action="/">
      <select class="select-input" name="category">
        {''.join(category_options)}
      </select>
      <button class="button button-secondary" type="submit">Filtreaza</button>
    </form>
    <a class="button button-link" href="/folders">Vezi folderele</a>
    <a class="button button-link" href="/categories">Gestioneaza categoriile</a>
    <a class="button button-link" href="/unmatched">Vezi cazurile neclare</a>
  </div>
  <div class="table-wrap">
    <table id="manuscripts-table" class="data-table">
      <thead>
        <tr>
          <th>Manuscris</th>
          <th>Titlu</th>
          <th>Categorii</th>
          <th>Imagini</th>
          <th>Foldere</th>
          <th>Sursa</th>
          <th>Notite</th>
        </tr>
      </thead>
      <tbody>
        {''.join(rows) or '<tr><td colspan="7">Nu au fost gasite manuscrise pentru filtrul curent.</td></tr>'}
      </tbody>
    </table>
  </div>
</section>
"""
    return layout("Arhiva manuscriselor", "manuscripts", content)


def render_folders_page(snapshot: IndexSnapshot, status: str) -> bytes:
    ordered_folders = sorted(
        snapshot.folders.values(),
        key=lambda folder: (
            0 if folder.first_manuscript is not None else 1,
            folder.first_manuscript or 10**9,
            folder.name.lower(),
        ),
    )
    rows = []
    for folder in ordered_folders:
        issues = (
            f"<span class='warning-chip'>{folder.unmatched_count} neclare</span>"
            if folder.unmatched_count
            else "<span class='ok-chip'>curat</span>"
        )
        preview = escape(compact_manuscript_label(folder.manuscripts))
        notes = escape(folder.notes[:90] + ("..." if len(folder.notes) > 90 else ""))
        thematic_category = (
            render_category_badges([folder.thematic_category])
            if folder.thematic_category
            else "<span class='muted'>-</span>"
        )
        rows.append(
            f"""
<tr data-search="{escape(f'{folder.name} {folder.archive_label} {folder.display_title} {folder.thematic_category} {folder.notes} {" ".join(str(n) for n in folder.manuscripts)}'.lower())}">
  <td><a class="strong-link" href="/folder?{urlencode({'key': folder.key})}">{escape(folder.name)}</a></td>
  <td>{escape(archive_short_label(folder.archive_label))}</td>
  <td>{escape(folder.display_title or '-')}</td>
  <td>{thematic_category}</td>
  <td>{preview}</td>
  <td>{folder.image_count}</td>
  <td>{issues}</td>
  <td>{notes or '<span class="muted">-</span>'}</td>
</tr>
"""
        )

    content = f"""
{build_status_message(status)}
{render_summary(snapshot)}
<section class="panel">
  <div class="panel-head">
    <div>
      <p class="eyebrow">Structura de foldere</p>
      <h3>Foldere sursa</h3>
    </div>
    <form method="post" action="/refresh">
      <input type="hidden" name="next" value="/folders">
      <button class="button button-secondary" type="submit">Refresh index</button>
    </form>
  </div>
  <div class="toolbar">
    <input class="search-input" data-filter-input="folders-table" type="search" placeholder="Cauta dupa nume, titlu, numere sau notite">
    <a class="button button-link" href="/">Inapoi la manuscrise</a>
  </div>
  <div class="table-wrap">
    <table id="folders-table" class="data-table">
      <thead>
        <tr>
          <th>Folder</th>
          <th>Arhiva</th>
          <th>Titlu afisat</th>
          <th>Categorie tematica</th>
          <th>Manuscrise detectate</th>
          <th>Imagini</th>
          <th>Status</th>
          <th>Notite</th>
        </tr>
      </thead>
      <tbody>
        {''.join(rows) or '<tr><td colspan="8">Nu exista foldere.</td></tr>'}
      </tbody>
    </table>
  </div>
</section>
"""
    return layout("Foldere manuscrise", "folders", content)


def render_folder_page(folder: FolderRecord, snapshot: IndexSnapshot, status: str) -> bytes:
    manuscript_badges = "".join(
        f"<a class='badge-link' href='/manuscript?{urlencode({'number': number})}'>BJ-{number}</a>"
        for number in folder.manuscripts
    ) or "<span class='muted'>Nu au fost detectate manuscrise.</span>"
    thematic_badges = (
        render_category_badges([folder.thematic_category])
        if folder.thematic_category
        else "<span class='muted'>Fara categorie tematica dedusa automat.</span>"
    )

    groups = []
    for number in folder.manuscripts:
        images = folder.images_by_manuscript[number]
        groups.append(
            f"""
<section class="panel manuscript-group">
  <div class="panel-head compact">
    <div>
      <p class="eyebrow">Manuscris din folder</p>
      <h3><a class="strong-link" href="/manuscript?{urlencode({'number': number})}">BJ-{number}</a></h3>
    </div>
    <span class="count-chip">{len(images)} imagini</span>
  </div>
  <div class="image-grid">
    {''.join(render_image_card(image, show_folder=False) for image in images)}
  </div>
</section>
"""
        )

    unmatched = ""
    if folder.unmatched_images:
        unmatched = f"""
<section class="panel">
  <div class="panel-head compact">
    <div>
      <p class="eyebrow">Necesita verificare</p>
      <h3>Imagini neclare</h3>
    </div>
    <span class="warning-chip">{folder.unmatched_count} fisiere</span>
  </div>
  <div class="image-grid">
    {''.join(render_image_card(image, show_folder=False) for image in folder.unmatched_images)}
  </div>
</section>
"""

    content = f"""
{build_status_message(status)}
<section class="hero detail-hero">
  <div class="hero-copy">
    <p class="eyebrow">Folder sursa</p>
    <h2>{escape(folder.effective_title)}</h2>
    <p class="detail-path">Arhiva: {escape(folder.archive_label)} ({escape(archive_short_label(folder.archive_label))})</p>
    <p class="detail-path">{escape(str(folder.absolute_path))}</p>
    <div class="badge-row">{thematic_badges}</div>
    <div class="badge-row">{manuscript_badges}</div>
  </div>
  <div class="hero-actions">
    <a class="button button-link" href="/folders">Inapoi la foldere</a>
    <a class="button button-link" href="/">Inapoi la manuscrise</a>
    <form method="post" action="/folder/open">
      <input type="hidden" name="key" value="{escape(folder.key)}">
      <input type="hidden" name="next" value="/folder?{urlencode({'key': folder.key})}">
      <button class="button button-secondary" type="submit">Deschide in Explorer</button>
    </form>
  </div>
</section>
<section class="panel">
  <div class="panel-head">
    <div>
      <p class="eyebrow">Editare locala</p>
      <h3>Descriere folder</h3>
    </div>
    <div class="panel-meta">
      <span class="count-chip">{folder.image_count} imagini</span>
      <span class="count-chip">{len(folder.manuscripts)} manuscrise</span>
    </div>
  </div>
  <form class="edit-form" method="post" action="/folder/edit">
    <input type="hidden" name="key" value="{escape(folder.key)}">
    <input type="hidden" name="next" value="/folder?{urlencode({'key': folder.key})}">
    <label>
      <span>Titlu afisat</span>
      <input type="text" name="display_title" value="{escape(folder.display_title)}" placeholder="Ex: Studii geometrice, caiet mixt">
    </label>
    <label>
      <span>Notite</span>
      <textarea name="notes" rows="4" placeholder="Noteaza aici ce contine folderul, exceptii sau observatii.">{escape(folder.notes)}</textarea>
    </label>
    <button class="button" type="submit">Salveaza</button>
  </form>
</section>
{''.join(groups)}
{unmatched}
"""
    return layout(f"Folder {folder.name}", "folders", content)


def render_manuscript_page(manuscript: ManuscriptRecord, snapshot: IndexSnapshot, status: str) -> bytes:
    grouped: dict[str, list[ImageEntry]] = {}
    for image in manuscript.images:
        grouped.setdefault(image.folder_key, []).append(image)

    categories_datalist = render_category_datalist(
        "manuscript-categories-list", list(snapshot.categories)
    )
    folder_sections = []
    for folder_key in sorted(grouped):
        folder = snapshot.folders[folder_key]
        images = grouped[folder_key]
        label = f"{folder.name} | {archive_short_label(folder.archive_label)}"
        folder_sections.append(
            f"""
<section class="panel manuscript-group">
  <div class="panel-head compact">
    <div>
      <p class="eyebrow">Sursa</p>
      <h3><a class="strong-link" href="/folder?{urlencode({'key': folder.key})}">{escape(label)}</a></h3>
    </div>
    <span class="count-chip">{len(images)} imagini</span>
  </div>
  <div class="image-grid">
    {''.join(render_image_card(image, show_folder=False) for image in images)}
  </div>
</section>
"""
        )

    content = f"""
{build_status_message(status)}
<section class="hero detail-hero">
  <div class="hero-copy">
    <p class="eyebrow">Manuscris</p>
    <h2>BJ-{manuscript.number}</h2>
    <p>{escape(manuscript.title or 'Fara titlu salvat inca.')}</p>
    <div class="badge-row">{render_category_badges(manuscript.categories)}</div>
    <div class="badge-row">
      {''.join(f"<a class='badge-link' href='/folder?{urlencode({'key': key})}'>{escape(snapshot.folders[key].name + ' | ' + archive_short_label(snapshot.folders[key].archive_label))}</a>" for key in sorted(manuscript.folders))}
    </div>
  </div>
  <div class="hero-actions">
    <a class="button button-link" href="/">Inapoi la manuscrise</a>
    <a class="button button-link" href="/categories">Categorii</a>
    <a class="button button-link" href="/folders">Vezi folderele</a>
    <a class="button button-link" href="/manuscript/download?{urlencode({'number': manuscript.number})}">Descarca toate paginile cotei</a>
    <a class="button button-link" href="{TRANSKRIBUS_URL}" target="_blank" rel="noreferrer">Transcriere cu Transkribus</a>
  </div>
</section>
<section class="panel">
  <div class="panel-head">
    <div>
      <p class="eyebrow">Editare locala</p>
      <h3>Descriere manuscris</h3>
    </div>
    <div class="panel-meta">
      <span class="count-chip">{len(manuscript.images)} imagini</span>
      <span class="count-chip">{len(manuscript.folders)} foldere</span>
    </div>
  </div>
  <form class="edit-form" method="post" action="/manuscript/edit">
    <input type="hidden" name="number" value="{manuscript.number}">
    <input type="hidden" name="next" value="/manuscript?{urlencode({'number': manuscript.number})}">
    <label>
      <span>Titlu</span>
      <input type="text" name="title" value="{escape(manuscript.title)}" placeholder="Ex: Caiet despre analiza, note de lucru, schite">
    </label>
    <label>
      <span>Categorii</span>
      <input type="text" name="categories" list="manuscript-categories-list" value="{escape(', '.join(manuscript.editable_categories))}" placeholder="Ex: filosofie, matematica, fragmente">
    </label>
    <label>
      <span>Adauga categorie noua</span>
      <input type="text" name="new_category" list="manuscript-categories-list" placeholder="Ex: geometrie, algebra, corespondenta">
    </label>
    <p class="form-hint">Poti scrie aici una sau mai multe categorii noi. La salvare, ele se adauga automat si in lista globala.</p>
    {f"<p class='form-hint'>Categorie tematica dedusa din folder: {render_category_badges(manuscript.auto_categories, linked=False)}</p>" if manuscript.auto_categories else ""}
    <p class="form-hint">Categorii disponibile: {render_category_badges(list(snapshot.categories), linked=False)}</p>
    {categories_datalist}
    <label>
      <span>Notite</span>
      <textarea name="notes" rows="4" placeholder="Completeaza aici identificarea manuscrisului.">{escape(manuscript.notes)}</textarea>
    </label>
    <button class="button" type="submit">Salveaza</button>
  </form>
  <div class="notes-block">{paragraphize(manuscript.notes)}</div>
</section>
{''.join(folder_sections)}
"""
    return layout(f"Manuscris BJ-{manuscript.number}", "manuscripts", content)


def render_categories_page(snapshot: IndexSnapshot, status: str) -> bytes:
    rows = []
    for category in snapshot.categories.values():
        preview = " ".join(
            f"<a class='badge-link' href='/manuscript?{urlencode({'number': number})}'>BJ-{number}</a>"
            for number in category.manuscripts[:12]
        ) or "<span class='muted'>nefolosita inca</span>"
        rows.append(
            f"""
<tr data-search="{escape(f'{category.name} {" ".join(str(number) for number in category.manuscripts)}'.lower())}">
  <td><a class="strong-link" href="/?{urlencode({'category': category.name})}">{escape(category.name)}</a></td>
  <td>{category.count}</td>
  <td>{preview}</td>
</tr>
"""
        )

    textarea_value = "\n".join(snapshot.categories)
    categories_datalist = render_category_datalist(
        "global-categories-list", list(snapshot.categories)
    )
    content = f"""
{build_status_message(status)}
{render_summary(snapshot)}
<section class="panel">
  <div class="panel-head">
    <div>
      <p class="eyebrow">Administrare</p>
      <h3>Lista de categorii</h3>
    </div>
    <a class="button button-link" href="/">Inapoi la manuscrise</a>
  </div>
  <form class="toolbar-form" method="post" action="/categories/add">
    <input type="hidden" name="next" value="/categories">
    <input class="search-input" type="text" name="category" list="global-categories-list" placeholder="Adauga rapid o categorie noua, ex: geometrie">
    <button class="button" type="submit">Adauga categorie</button>
  </form>
  <p class="muted">Pentru o categorie noua poti folosi formularul rapid de mai sus, iar pentru reorganizare larga poti edita lista completa de mai jos.</p>
  <p class="muted">Pune cate o categorie pe linie separata. Categoriile deja folosite de manuscrise raman pastrate automat.</p>
  <form class="edit-form" method="post" action="/categories/save">
    <input type="hidden" name="next" value="/categories">
    <label>
      <span>Categorii disponibile</span>
      <textarea name="categories" rows="10" placeholder="Ex: filosofie&#10;matematica&#10;fragmente">{escape(textarea_value)}</textarea>
    </label>
    <button class="button" type="submit">Salveaza categoriile</button>
  </form>
  {categories_datalist}
</section>
<section class="panel">
  <div class="panel-head">
    <div>
      <p class="eyebrow">Triere</p>
      <h3>Categorii existente</h3>
    </div>
  </div>
  <div class="toolbar">
    <input class="search-input" data-filter-input="categories-table" type="search" placeholder="Cauta dupa categorie sau numar de manuscris">
  </div>
  <div class="table-wrap">
    <table id="categories-table" class="data-table">
      <thead>
        <tr>
          <th>Categorie</th>
          <th>Manuscrise</th>
          <th>Exemple</th>
        </tr>
      </thead>
      <tbody>
        {''.join(rows) or '<tr><td colspan="3">Nu exista categorii salvate inca.</td></tr>'}
      </tbody>
    </table>
  </div>
</section>
"""
    return layout("Categorii manuscrise", "categories", content)


def build_manuscript_archive(manuscript: ManuscriptRecord) -> tuple[bytes, str]:
    archive_name = f"BJ-{manuscript.number}.zip"
    buffer = io.BytesIO()
    with zipfile.ZipFile(buffer, "w", compression=zipfile.ZIP_DEFLATED) as archive:
        for image in manuscript.images:
            internal_path = "/".join(
                [
                    f"BJ-{manuscript.number}",
                    safe_download_name(image.archive_label),
                    image.relative_path.replace("\\", "/"),
                ]
            )
            archive.writestr(internal_path, image.absolute_path.read_bytes())
    return buffer.getvalue(), archive_name


def render_image_viewer_page(
    image: ImageEntry,
    previous_image: ImageEntry | None = None,
    next_image: ImageEntry | None = None,
    navigation_scope: str = "",
    status: str = "",
) -> bytes:
    manuscript_label = (
        f"BJ-{image.manuscript_number}"
        if image.manuscript_number is not None
        else "Neidentificat"
    )
    manuscript_link = (
        f"/manuscript?{urlencode({'number': image.manuscript_number})}"
        if image.manuscript_number is not None
        else "/unmatched"
    )
    back_label = (
        "Inapoi la cota" if image.manuscript_number is not None else "Inapoi la imagini neclare"
    )
    download_url = f"/image?{urlencode({'path': image.resource_key, 'download': '1'})}"
    previous_url = previous_image.viewer_url if previous_image else ""
    next_url = next_image.viewer_url if next_image else ""
    previous_arrow = (
        f'<a class="viewer-arrow viewer-arrow-left" href="{escape(previous_url)}" '
        'aria-label="Imaginea precedenta">&larr;</a>'
        if previous_image
        else '<span class="viewer-arrow viewer-arrow-left is-disabled" '
        'aria-hidden="true">&larr;</span>'
    )
    next_arrow = (
        f'<a class="viewer-arrow viewer-arrow-right" href="{escape(next_url)}" '
        'aria-label="Imaginea urmatoare">&rarr;</a>'
        if next_image
        else '<span class="viewer-arrow viewer-arrow-right is-disabled" '
        'aria-hidden="true">&rarr;</span>'
    )
    navigation_text = (
        f"{navigation_scope}. Folositi sagetile din ecran sau tastele stanga/dreapta."
        if previous_image or next_image
        else "Nu exista alta imagine in aceasta selectie."
    )
    content = f"""
{build_status_message(status)}
<section class="hero detail-hero">
  <div class="hero-copy">
    <p class="eyebrow">Vizualizare imagine</p>
    <h2>{escape(image.filename)}</h2>
    <p class="detail-path">{escape(str(image.absolute_path))}</p>
    <div class="badge-row">
      <a class="badge-link" href="{escape(manuscript_link)}">{escape(manuscript_label)}</a>
      <a class="badge-link" href="/folder?{urlencode({'key': image.folder_key})}">{escape(image.folder_name + ' | ' + archive_short_label(image.archive_label))}</a>
      <span class="badge-chip">{escape(image.page_label or 'pagina neidentificata')}</span>
    </div>
  </div>
  <div class="hero-actions">
    <a class="button button-link" href="{download_url}">Descarca imaginea</a>
    <a class="button button-link" href="{image.image_url}" target="_blank" rel="noreferrer">Deschide originalul</a>
    <a class="button button-link" href="/folder?{urlencode({'key': image.folder_key})}">Inapoi la folder</a>
    <a class="button button-link" href="{escape(manuscript_link)}">{escape(back_label)}</a>
  </div>
</section>
<section class="panel viewer-panel">
  <div class="panel-head">
    <div>
      <p class="eyebrow">Corectie rapida</p>
      <h3>Rotire la 90 de grade</h3>
    </div>
    <div class="viewer-toolbar">
      <button class="button button-secondary" type="button" id="rotate-left">Roteste stanga 90°</button>
      <button class="button button-secondary" type="button" id="rotate-right">Roteste dreapta 90°</button>
      <button class="button button-secondary" type="button" id="toggle-loupe" aria-pressed="false">Lupa text</button>
      <span class="count-chip" id="rotation-state">0°</span>
    </div>
  </div>
  <div class="viewer-canvas">
    <div id="viewer-stage" class="viewer-stage">
      {previous_arrow}
      <img id="viewer-image" class="viewer-image" src="{image.image_url}" alt="{escape(image.filename)}">
      {next_arrow}
      <div id="viewer-loupe" class="viewer-loupe is-hidden" aria-hidden="true">
        <canvas id="viewer-loupe-canvas" class="viewer-loupe-canvas" width="220" height="220"></canvas>
      </div>
    </div>
  </div>
  <p class="muted">Rotirea este pentru vizualizare in browser si nu modifica fisierul original.</p>
  <p class="muted">Activati lupa si miscati cursorul pe imagine atunci cand aveti nevoie de text marit.</p>
  <p class="muted">{escape(navigation_text)}</p>
</section>
<section class="panel">
  <div class="panel-head compact">
    <div>
      <p class="eyebrow">Transcriere electronica</p>
      <h3>Transkribus</h3>
    </div>
    <a class="button button-link" href="{TRANSKRIBUS_URL}" target="_blank" rel="noreferrer">Deschide Transkribus</a>
  </div>
  <p class="muted">Pentru cine doreste sa transcrie o foaie de manuscris, pagina poate fi incarcata in Transkribus.</p>
  <p class="muted">Incarcarea unei pagini de manuscris acolo permite reproducerea si transcrierea ei electronica.</p>
</section>
<script>
  (() => {{
    const image = document.getElementById('viewer-image');
    const stage = document.getElementById('viewer-stage');
    const state = document.getElementById('rotation-state');
    const leftButton = document.getElementById('rotate-left');
    const rightButton = document.getElementById('rotate-right');
    const loupeButton = document.getElementById('toggle-loupe');
    const loupe = document.getElementById('viewer-loupe');
    const loupeCanvas = document.getElementById('viewer-loupe-canvas');
    const loupeContext = loupeCanvas.getContext('2d');
    const previousUrl = {json.dumps(previous_url)};
    const nextUrl = {json.dumps(next_url)};
    const loupeSize = loupeCanvas.width;
    const loupeRadius = loupeSize / 2;
    const loupeZoom = 2.6;
    let angle = 0;
    let loupeEnabled = false;
    const clamp = (value, minimum, maximum) => Math.min(Math.max(value, minimum), maximum);
    const normalizedAngle = () => ((angle % 360) + 360) % 360;
    const syncStageSize = () => {{
      const rect = image.getBoundingClientRect();
      if (!rect.width || !rect.height) {{
        return;
      }}
      stage.style.width = `${{Math.ceil(rect.width)}}px`;
      stage.style.height = `${{Math.ceil(rect.height)}}px`;
    }};
    const render = () => {{
      image.style.transform = `rotate(${{angle}}deg)`;
      state.textContent = `${{normalizedAngle()}}°`;
      window.requestAnimationFrame(syncStageSize);
      if (loupeEnabled) {{
        loupe.classList.add('is-hidden');
      }}
    }};
    const getImagePoint = (clientX, clientY) => {{
      const rect = image.getBoundingClientRect();
      const displayWidth = image.clientWidth;
      const displayHeight = image.clientHeight;
      if (
        !rect.width ||
        !rect.height ||
        !displayWidth ||
        !displayHeight ||
        !image.naturalWidth ||
        !image.naturalHeight
      ) {{
        return null;
      }}
      if (
        clientX < rect.left ||
        clientX > rect.right ||
        clientY < rect.top ||
        clientY > rect.bottom
      ) {{
        return null;
      }}
      const boxX = clientX - rect.left;
      const boxY = clientY - rect.top;
      let imageX = boxX;
      let imageY = boxY;
      switch (normalizedAngle()) {{
        case 90:
          imageX = boxY;
          imageY = displayHeight - boxX;
          break;
        case 180:
          imageX = displayWidth - boxX;
          imageY = displayHeight - boxY;
          break;
        case 270:
          imageX = displayWidth - boxY;
          imageY = boxX;
          break;
        default:
          break;
      }}
      imageX = clamp(imageX, 0, displayWidth);
      imageY = clamp(imageY, 0, displayHeight);
      return {{
        sourceX: imageX * (image.naturalWidth / displayWidth),
        sourceY: imageY * (image.naturalHeight / displayHeight),
      }};
    }};
    const drawLoupe = (sourceX, sourceY) => {{
      if (!loupeContext) {{
        return;
      }}
      const displayWidth = image.clientWidth;
      const displayHeight = image.clientHeight;
      if (!displayWidth || !displayHeight) {{
        return;
      }}
      const scaleX = displayWidth / image.naturalWidth;
      const scaleY = displayHeight / image.naturalHeight;
      const sampleWidth = loupeSize / (loupeZoom * scaleX);
      const sampleHeight = loupeSize / (loupeZoom * scaleY);
      const sampleX = clamp(
        sourceX - sampleWidth / 2,
        0,
        Math.max(image.naturalWidth - sampleWidth, 0)
      );
      const sampleY = clamp(
        sourceY - sampleHeight / 2,
        0,
        Math.max(image.naturalHeight - sampleHeight, 0)
      );
      loupeContext.clearRect(0, 0, loupeSize, loupeSize);
      loupeContext.save();
      loupeContext.beginPath();
      loupeContext.arc(loupeRadius, loupeRadius, loupeRadius - 2, 0, Math.PI * 2);
      loupeContext.clip();
      loupeContext.fillStyle = '#fffaf2';
      loupeContext.fillRect(0, 0, loupeSize, loupeSize);
      loupeContext.translate(loupeRadius, loupeRadius);
      loupeContext.rotate((normalizedAngle() * Math.PI) / 180);
      loupeContext.drawImage(
        image,
        sampleX,
        sampleY,
        sampleWidth,
        sampleHeight,
        -loupeRadius,
        -loupeRadius,
        loupeSize,
        loupeSize
      );
      loupeContext.restore();
      loupeContext.save();
      loupeContext.strokeStyle = 'rgba(104, 58, 20, 0.35)';
      loupeContext.lineWidth = 1;
      loupeContext.beginPath();
      loupeContext.moveTo(loupeRadius, 14);
      loupeContext.lineTo(loupeRadius, loupeSize - 14);
      loupeContext.moveTo(14, loupeRadius);
      loupeContext.lineTo(loupeSize - 14, loupeRadius);
      loupeContext.stroke();
      loupeContext.restore();
    }};
    const positionLoupe = (clientX, clientY) => {{
      const rect = stage.getBoundingClientRect();
      const stageX = clientX - rect.left;
      const stageY = clientY - rect.top;
      const horizontalRadius = Math.min(loupeRadius, rect.width / 2);
      const verticalRadius = Math.min(loupeRadius, rect.height / 2);
      const left = clamp(
        stageX + 34,
        horizontalRadius,
        Math.max(horizontalRadius, rect.width - horizontalRadius)
      );
      const top = clamp(
        stageY - 34,
        verticalRadius,
        Math.max(verticalRadius, rect.height - verticalRadius)
      );
      loupe.style.left = `${{left}}px`;
      loupe.style.top = `${{top}}px`;
    }};
    const hideLoupe = () => {{
      loupe.classList.add('is-hidden');
    }};
    const updateLoupe = (event) => {{
      if (!loupeEnabled || event.pointerType === 'touch') {{
        hideLoupe();
        return;
      }}
      const point = getImagePoint(event.clientX, event.clientY);
      if (!point) {{
        hideLoupe();
        return;
      }}
      positionLoupe(event.clientX, event.clientY);
      drawLoupe(point.sourceX, point.sourceY);
      loupe.classList.remove('is-hidden');
    }};
    leftButton.addEventListener('click', () => {{
      angle -= 90;
      render();
    }});
    rightButton.addEventListener('click', () => {{
      angle += 90;
      render();
    }});
    loupeButton.addEventListener('click', () => {{
      loupeEnabled = !loupeEnabled;
      stage.classList.toggle('is-loupe-active', loupeEnabled);
      loupeButton.setAttribute('aria-pressed', loupeEnabled ? 'true' : 'false');
      loupeButton.textContent = loupeEnabled ? 'Lupa activa' : 'Lupa text';
      if (!loupeEnabled) {{
        hideLoupe();
      }}
    }});
    stage.addEventListener('pointermove', updateLoupe);
    stage.addEventListener('pointerleave', hideLoupe);
    image.addEventListener('load', syncStageSize);
    window.addEventListener('resize', syncStageSize);
    document.addEventListener('keydown', (event) => {{
      if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) {{
        return;
      }}
      const activeTag = document.activeElement ? document.activeElement.tagName : '';
      if (['BUTTON', 'INPUT', 'SELECT', 'TEXTAREA'].includes(activeTag)) {{
        return;
      }}
      if (event.key === 'ArrowLeft' && previousUrl) {{
        window.location.href = previousUrl;
      }}
      if (event.key === 'ArrowRight' && nextUrl) {{
        window.location.href = nextUrl;
      }}
    }});
    render();
    if (image.complete) {{
      syncStageSize();
    }}
  }})();
</script>
"""
    return layout(f"Vizualizare {image.filename}", "manuscripts", content)


def render_unmatched_page(snapshot: IndexSnapshot, status: str) -> bytes:
    sections = []
    for folder in snapshot.folders.values():
        if not folder.unmatched_images:
            continue
        sections.append(
            f"""
<section class="panel">
  <div class="panel-head compact">
    <div>
      <p class="eyebrow">Folder cu ambiguitati</p>
      <h3><a class="strong-link" href="/folder?{urlencode({'key': folder.key})}">{escape(folder.name + ' | ' + archive_short_label(folder.archive_label))}</a></h3>
    </div>
    <span class="warning-chip">{folder.unmatched_count} neclare</span>
  </div>
  <div class="image-grid">
    {''.join(render_image_card(image, show_folder=False) for image in folder.unmatched_images)}
  </div>
</section>
"""
        )

    if not sections:
        sections.append(
            """
<section class="panel">
  <div class="panel-head compact">
    <div>
      <p class="eyebrow">Curatare</p>
      <h3>Nu exista imagini neclare</h3>
    </div>
  </div>
  <p class="muted">Toate imaginile au fost asociate unui manuscris pe baza numelui fisierului.</p>
</section>
"""
        )

    content = f"""
{build_status_message(status)}
{render_summary(snapshot)}
<section class="panel">
  <div class="panel-head">
    <div>
      <p class="eyebrow">Verificare manuala</p>
      <h3>Imagini care nu au putut fi identificate automat</h3>
    </div>
    <a class="button button-link" href="/folders">Vezi folderele</a>
  </div>
  <p class="muted">Aici apar fisiere precum imagini `DSC...` sau alte nume care nu includ explicit numarul manuscrisului.</p>
</section>
{''.join(sections)}
"""
    return layout("Imagini neclare", "unmatched", content)


def render_image_card(image: ImageEntry, show_folder: bool = True) -> str:
    folder_meta = (
        f"<p class='image-meta'><a href='/folder?{urlencode({'key': image.folder_key})}'>{escape(image.folder_name + ' | ' + archive_short_label(image.archive_label))}</a></p>"
        if show_folder
        else ""
    )
    label = image.page_label or "pagina neidentificata"
    manuscript = (
        f"BJ-{image.manuscript_number}" if image.manuscript_number is not None else "neclar"
    )
    return f"""
<article class="image-card">
  <a class="image-link" href="{image.viewer_url}">
    <img loading="lazy" src="{image.image_url}" alt="{escape(image.filename)}">
  </a>
  <div class="image-copy">
    <p class="image-title">{escape(image.filename)}</p>
    <p class="image-meta">{escape(manuscript)} | {escape(label)}</p>
    {folder_meta}
  </div>
</article>
"""


class ManuscriptRequestHandler(BaseHTTPRequestHandler):
    server_version = "MSBJBrowser/1.0"

    @property
    def app(self) -> "ManuscriptBrowserServer":
        return self.server  # type: ignore[return-value]

    def do_GET(self) -> None:
        parsed = urlparse(self.path)
        params = parse_qs(parsed.query)
        status = params.get("status", [""])[0]
        selected_category = normalize_category_name(params.get("category", [""])[0])

        if parsed.path == "/":
            self._send_html(
                render_manuscripts_page(
                    self.app.index.get_snapshot(), status, selected_category
                )
            )
            return
        if parsed.path == "/folders":
            self._send_html(render_folders_page(self.app.index.get_snapshot(), status))
            return
        if parsed.path == "/categories":
            self._send_html(render_categories_page(self.app.index.get_snapshot(), status))
            return
        if parsed.path == "/folder":
            key = params.get("key", [""])[0]
            folder = self.app.index.get_folder(key) if key else None
            if not folder and params.get("name", [""])[0]:
                matches = self.app.index.find_folders_by_name(params.get("name", [""])[0])
                folder = matches[0] if len(matches) == 1 else None
            if not folder:
                self._redirect("/folders?status=invalid-folder")
                return
            self._send_html(render_folder_page(folder, self.app.index.get_snapshot(), status))
            return
        if parsed.path == "/manuscript":
            raw_number = params.get("number", [""])[0]
            if not raw_number.isdigit():
                self._redirect("/?status=invalid-manuscript")
                return
            manuscript = self.app.index.get_manuscript(int(raw_number))
            if not manuscript:
                self._redirect("/?status=invalid-manuscript")
                return
            self._send_html(
                render_manuscript_page(manuscript, self.app.index.get_snapshot(), status)
            )
            return
        if parsed.path == "/unmatched":
            self._send_html(render_unmatched_page(self.app.index.get_snapshot(), status))
            return
        if parsed.path == "/viewer":
            resource_key = params.get("path", [""])[0]
            image = self.app.index.get_image(resource_key) if resource_key else None
            if not image:
                self._redirect("/unmatched?status=invalid-image")
                return
            previous_image, next_image, navigation_scope = self.app.index.get_viewer_navigation(
                image
            )
            self._send_html(
                render_image_viewer_page(
                    image,
                    previous_image=previous_image,
                    next_image=next_image,
                    navigation_scope=navigation_scope,
                    status=status,
                )
            )
            return
        if parsed.path == "/manuscript/download":
            raw_number = params.get("number", [""])[0]
            if not raw_number.isdigit():
                self._redirect("/?status=invalid-manuscript")
                return
            manuscript = self.app.index.get_manuscript(int(raw_number))
            if not manuscript:
                self._redirect("/?status=invalid-manuscript")
                return
            payload, filename = build_manuscript_archive(manuscript)
            self._send_bytes(payload, "application/zip", filename)
            return
        if parsed.path == "/image":
            relative_path = params.get("path", [""])[0]
            path = self.app.index.resolve_relative_path(relative_path)
            if not path:
                self.send_error(HTTPStatus.NOT_FOUND, "Imaginea ceruta nu a fost gasita.")
                return
            download_name = path.name if params.get("download", [""])[0] == "1" else None
            self._send_file(path, download_name=download_name)
            return
        if parsed.path == "/static/style.css":
            self._send_static(self.app.style_path, "text/css; charset=utf-8")
            return

        self.send_error(HTTPStatus.NOT_FOUND, "Ruta necunoscuta.")

    def do_POST(self) -> None:
        parsed = urlparse(self.path)
        form = self._read_form()
        next_url = normalize_next_url(form.get("next", ["/"])[0])

        if parsed.path == "/refresh":
            self.app.index.refresh()
            self._redirect(add_status(next_url, "refreshed"))
            return

        if parsed.path == "/folder/edit":
            key = form.get("key", [""])[0]
            folder = self.app.index.get_folder(key)
            if not folder:
                self._redirect(add_status("/folders", "invalid-folder"))
                return
            self.app.index.save_folder_metadata(
                key,
                form.get("display_title", [""])[0],
                form.get("notes", [""])[0],
            )
            self._redirect(add_status(next_url, "folder-saved"))
            return

        if parsed.path == "/manuscript/edit":
            raw_number = form.get("number", [""])[0]
            if not raw_number.isdigit() or not self.app.index.get_manuscript(int(raw_number)):
                self._redirect(add_status("/", "invalid-manuscript"))
                return
            categories = merge_categories(
                form.get("categories", [""])[0],
                form.get("new_category", [""])[0],
            )
            self.app.index.save_manuscript_metadata(
                int(raw_number),
                form.get("title", [""])[0],
                form.get("notes", [""])[0],
                categories,
            )
            self._redirect(add_status(next_url, "manuscript-saved"))
            return

        if parsed.path == "/categories/add":
            if self.app.index.add_category(form.get("category", [""])[0]):
                self._redirect(add_status(next_url or "/categories", "category-added"))
            else:
                self._redirect(add_status(next_url or "/categories", "invalid-category"))
            return

        if parsed.path == "/categories/save":
            self.app.index.save_categories(form.get("categories", [""])[0])
            self._redirect(add_status(next_url or "/categories", "categories-saved"))
            return

        if parsed.path == "/folder/open":
            key = form.get("key", [""])[0]
            folder = self.app.index.get_folder(key)
            if not folder:
                self._redirect(add_status("/folders", "invalid-folder"))
                return
            if hasattr(os, "startfile"):
                os.startfile(str(folder.absolute_path))  # type: ignore[attr-defined]
            self._redirect(add_status(next_url, "folder-opened"))
            return

        self.send_error(HTTPStatus.NOT_FOUND, "Actiune necunoscuta.")

    def log_message(self, format: str, *args: Any) -> None:
        sys.stderr.write(
            "%s - - [%s] %s\n"
            % (self.address_string(), self.log_date_time_string(), format % args)
        )

    def _read_form(self) -> dict[str, list[str]]:
        content_length = int(self.headers.get("Content-Length", "0"))
        raw = self.rfile.read(content_length).decode("utf-8", errors="replace")
        return parse_qs(raw, keep_blank_values=True)

    def _send_html(self, payload: bytes, status: HTTPStatus = HTTPStatus.OK) -> None:
        self.send_response(status)
        self.send_header("Content-Type", "text/html; charset=utf-8")
        self.send_header("Content-Length", str(len(payload)))
        self.end_headers()
        self.wfile.write(payload)

    def _send_static(self, path: Path, content_type: str) -> None:
        payload = path.read_bytes()
        self.send_response(HTTPStatus.OK)
        self.send_header("Content-Type", content_type)
        self.send_header("Content-Length", str(len(payload)))
        self.end_headers()
        self.wfile.write(payload)

    def _send_bytes(
        self,
        payload: bytes,
        content_type: str,
        download_name: str | None = None,
    ) -> None:
        self.send_response(HTTPStatus.OK)
        self.send_header("Content-Type", content_type)
        self.send_header("Content-Length", str(len(payload)))
        if download_name:
            self.send_header(
                "Content-Disposition",
                f'attachment; filename="{safe_download_name(download_name)}"',
            )
        self.end_headers()
        self.wfile.write(payload)

    def _send_file(self, path: Path, download_name: str | None = None) -> None:
        content_type, _ = mimetypes.guess_type(path.name)
        self.send_response(HTTPStatus.OK)
        self.send_header("Content-Type", content_type or "application/octet-stream")
        self.send_header("Content-Length", str(path.stat().st_size))
        if download_name:
            self.send_header(
                "Content-Disposition",
                f'attachment; filename="{safe_download_name(download_name)}"',
            )
        self.end_headers()
        with path.open("rb") as source:
            while True:
                chunk = source.read(1024 * 64)
                if not chunk:
                    break
                self.wfile.write(chunk)

    def _redirect(self, target: str) -> None:
        self.send_response(HTTPStatus.SEE_OTHER)
        self.send_header("Location", target)
        self.end_headers()


class ManuscriptBrowserServer(ThreadingHTTPServer):
    allow_reuse_address = True

    def __init__(
        self,
        server_address: tuple[str, int],
        index: LibraryIndex,
        style_path: Path,
    ):
        self.index = index
        self.style_path = style_path
        self.url_scheme = "http"
        self.certfile: Path | None = None
        self.keyfile: Path | None = None
        super().__init__(server_address, ManuscriptRequestHandler)


def add_status(url: str, status: str) -> str:
    parsed = urlparse(url)
    query = parse_qs(parsed.query)
    query["status"] = [status]
    encoded = urlencode(query, doseq=True)
    return parsed._replace(query=encoded).geturl()


def normalize_next_url(next_url: str) -> str:
    if not next_url.startswith("/"):
        return "/"
    return next_url


def build_server(
    host: str,
    port: int,
    root: Path,
    metadata_path: Path | None = None,
    certfile: Path | None = None,
    keyfile: Path | None = None,
) -> ManuscriptBrowserServer:
    resolved_root = validate_root(root)
    resolved_port = validate_port(port)
    resolved_certfile, resolved_keyfile = validate_tls_files(certfile, keyfile)
    style_path = Path(__file__).resolve().parent / "static" / "style.css"
    resolved_metadata_path = (
        metadata_path.expanduser().resolve()
        if metadata_path is not None
        else default_metadata_path()
    )
    index = LibraryIndex(resolved_root, resolved_metadata_path)
    http_server = ManuscriptBrowserServer((host, resolved_port), index, style_path)
    if resolved_certfile is not None:
        try:
            context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
            context.load_cert_chain(
                certfile=str(resolved_certfile),
                keyfile=str(resolved_keyfile) if resolved_keyfile else None,
            )
            http_server.socket = context.wrap_socket(
                http_server.socket, server_side=True
            )
        except ssl.SSLError as exc:
            http_server.server_close()
            raise ValueError(
                f"Configuratie HTTPS invalida: {exc}"
            ) from exc
        http_server.url_scheme = "https"
        http_server.certfile = resolved_certfile
        http_server.keyfile = resolved_keyfile
    return http_server


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="Server local pentru arhiva manuscriselor Bolyai."
    )
    parser.add_argument("--host", default="0.0.0.0", help="Adresa de ascultare.")
    parser.add_argument("--port", type=int, default=DEFAULT_PORT, help="Portul HTTP.")
    parser.add_argument(
        "--root",
        type=Path,
        default=DEFAULT_ROOT,
        help="Directorul de baza. Daca contine pachete MS_BJ-..., le indexeaza pe toate.",
    )
    parser.add_argument(
        "--metadata",
        type=Path,
        default=None,
        help="Fisierul JSON in care se salveaza metadatele editabile.",
    )
    parser.add_argument(
        "--certfile",
        type=Path,
        default=None,
        help="Fisier certificat PEM/CRT pentru pornire HTTPS.",
    )
    parser.add_argument(
        "--keyfile",
        type=Path,
        default=None,
        help="Fisier cheie privata PEM daca nu este inclus in certificatul principal.",
    )
    parser.add_argument(
        "--no-firewall",
        action="store_true",
        help="Nu incearca sa adauge regula automata in Windows Firewall.",
    )
    return parser.parse_args()


def main() -> int:
    args = parse_args()
    try:
        server = build_server(
            args.host,
            args.port,
            args.root,
            args.metadata,
            args.certfile,
            args.keyfile,
        )
    except (ValueError, OSError) as exc:
        print(str(exc), file=sys.stderr)
        return 1

    host, port = server.server_address
    scheme = server.url_scheme
    print(f"Server pornit pe {build_bound_url(scheme, host, port)}")
    print(f"Deschidere locala: {build_local_url(scheme, port)}")
    print(f"Indexare din: {server.index.root}")
    print(f"Metadate salvate in: {server.index.metadata_path}")
    if server.certfile is not None:
        print(f"HTTPS activ cu certificat: {server.certfile}")
        if server.keyfile is not None:
            print(f"Cheie privata folosita: {server.keyfile}")
    if should_configure_firewall(args.host, not args.no_firewall):
        firewall_ok, firewall_message = ensure_windows_firewall_rule(port)
        print(
            firewall_message,
            file=sys.stdout if firewall_ok else sys.stderr,
        )
    try:
        server.serve_forever()
    except KeyboardInterrupt:
        print("\nServer oprit.")
    finally:
        server.server_close()
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
