import asyncio
import logging
import re
import shutil
import tempfile
import zipfile
from pathlib import Path
from typing import Dict, Iterable, List, Optional, Tuple

# ===== Bezpieczeństwo nazw/rozszerzeń =====
SAFE_TEX_NAME = re.compile(r"^[\w\-. ]+\.tex$", re.IGNORECASE)
SAFE_ASSET_NAME = re.compile(
    r"^[\w\-. /]+?\.(tex|png|jpg|jpeg|pdf|svg|eps|bmp|gif|sty|cls|bib|bst|bbx|cbx|def|cfg)$",
    re.IGNORECASE,
)

def is_safe_tex_name(name: str) -> bool:
    return bool(SAFE_TEX_NAME.match(name or ""))

def is_safe_asset_name(name: str) -> bool:
    return bool(SAFE_ASSET_NAME.match(name or ""))

# ===== Uruchamianie kompilatora =====

async def _run(cmd: List[str], cwd: Path, timeout_sec: int, logger_name: Optional[str]) -> Tuple[int, str]:
    """Uruchamia proces kompilatora w cwd, zwraca (rc, sklejone_stdout_stderr)."""
    logger = logging.getLogger(logger_name) if logger_name else logging.getLogger()
    logger.debug("RUN %s (cwd=%s, timeout=%s)", " ".join(cmd), cwd, timeout_sec)

    proc = await asyncio.create_subprocess_exec(
        *cmd,
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.STDOUT,
        cwd=str(cwd),
    )
    try:
        out = await asyncio.wait_for(proc.communicate(), timeout=timeout_sec)
    except asyncio.TimeoutError:
        try:
            proc.kill()
        finally:
            pass
        return 124, "TIMEOUT: compiler took too long"
    rc = proc.returncode
    log = (out[0] or b"").decode("utf-8", errors="ignore")
    logger.debug("RC=%s, LOG TAIL:\n%s", rc, log[-1500:])
    return rc, log

def _build_cmd(tex_engine: str, main_tex: str) -> List[str]:
    """Buduje komendę kompilatora; domyślnie tectonic/pdflatex/xelatex."""
    if tex_engine.lower() == "tectonic":
        # Tectonic sam robi wielofazowość, ale i tak uruchamiamy 2x (bezpiecznie dla bib/refs)
        return ["tectonic", "--keep-intermediates", "--synctex", main_tex]
    elif tex_engine.lower() in ("pdflatex", "xelatex", "lualatex"):
        return [tex_engine, "-interaction=nonstopmode", "-halt-on-error", main_tex]
    else:
        # fallback – traktujemy jak pdflatex
        return ["pdflatex", "-interaction=nonstopmode", "-halt-on-error", main_tex]

async def run_latex_two_passes(
    tex_filename: str, workdir: Path, tex_engine: str, timeout_sec: int, logger: Optional[str] = None
) -> Tuple[bool, str, Path]:
    """
    Uruchamia kompilację 2 razy (bezpieczeństwo referencji/bib).
    Zwraca: (ok, log_text, pdf_path).
    """
    logger_obj = logging.getLogger(logger) if logger else logging.getLogger()
    logger_obj.info("Compiling %s with %s engine two passes", tex_filename, tex_engine)
    logs: List[str] = []
    pdf_path = workdir / (Path(tex_filename).stem + ".pdf")

    # PASS 1
    rc1, log1 = await _run(_build_cmd(tex_engine, tex_filename), workdir, timeout_sec, logger)
    logs.append(log1)
    if rc1 != 0:
        return False, "".join(logs), pdf_path

    # PASS 2 (często już nic nie robi, ale nie szkodzi)
    rc2, log2 = await _run(_build_cmd(tex_engine, tex_filename), workdir, timeout_sec, logger)
    logs.append(log2)
    ok = (rc2 == 0) and pdf_path.exists()
    return ok, "".join(logs), pdf_path

# ===== Kompilacje: pojedynczy TEX + ZIP =====

async def compile_single_tex_bytes(
    tex_bytes: bytes,
    filename: str,
    tex_engine: str,
    timeout_sec: int,
    logger: Optional[str] = None,
    support_files: Optional[Iterable[Tuple[str, bytes]]] = None,
) -> Dict[str, Optional[object]]:
    """
    Kompiluje pojedynczy .tex + dowolne pliki pomocnicze (obrazy, sty/cls/bib, inne .tex).
    Każdy plik zapisujemy w katalogu roboczym widocznym dla kompilatora.
    """
    log = logging.getLogger(logger) if logger else logging.getLogger()
    log.info("Compiling %s with %s engine single tex", filename, tex_engine)
    if not is_safe_tex_name(filename):
        return {"ok": False, "pdf_bytes": None, "log_text": "Invalid .tex filename", "pdf_name": None}

    with tempfile.TemporaryDirectory(prefix="latex_one_") as td:
        wd = Path(td)
        (wd / Path(filename).name).write_bytes(tex_bytes)

        # Zapisz wszystkie supporty obok głównego pliku
        if support_files:
            for fname, data in support_files:
                if not fname or not is_safe_asset_name(fname):
                    continue
                # spłaszczamy do nazwy bazowej (bez podkatalogów z zewn. źródeł)
                (wd / Path(fname).name).write_bytes(data)

        ok, log_text, pdf_path = await run_latex_two_passes(Path(filename).name, wd, tex_engine, timeout_sec, logger)
        log.info("LaTeX compile finished: %s", "OK" if ok else "FAIL")
        log.info("Log text:\n%s", log_text[-2000:])  # Log last 2000 chars
        log.info("PDF path: %s", pdf_path)
        if ok and pdf_path.exists():
            return {
                "ok": True,
                "pdf_bytes": pdf_path.read_bytes(),
                "log_text": log_text,
                "pdf_name": pdf_path.name,
            }
        return {"ok": False, "pdf_bytes": None, "log_text": log_text, "pdf_name": pdf_path.name}

async def compile_zip_to_zip(
    zip_bytes: bytes, tex_engine: str, timeout_sec: int, logger: Optional[str] = None
) -> Tuple[bytes, List[str]]:
    """
    Rozpakowuje ZIP, znajduję wszystkie .tex i dla KAŻDEGO tworzy osobny katalog roboczy
    ze skopiowanym CAŁYM drzewem, by zachować ścieżki względne obrazków/plików.
    Zwraca (zip_pdf_bytes, lista_rel_sciezek_które_padły).
    """
    log = logging.getLogger(logger) if logger else logging.getLogger()
    failed: List[str] = []
    out_pdf_paths: List[Path] = []

    with tempfile.TemporaryDirectory(prefix="latex_zip_") as td:
        td_path = Path(td)
        src_root = td_path / "src"
        src_root.mkdir(parents=True, exist_ok=True)

        # 1) Rozpakuj
        zpath = td_path / "in.zip"
        zpath.write_bytes(zip_bytes)
        with zipfile.ZipFile(zpath, "r") as zf:
            zf.extractall(src_root)

        # 2) Zbierz .tex
        tex_files = [p for p in src_root.rglob("*.tex") if is_safe_tex_name(p.name)]
        if not tex_files:
            return b"", ["No .tex files found in archive."]

        # 3) Kompiluj każdy .tex w izolacji (kopiujemy całe drzewo do osobnego workdiru)
        for tex_path in tex_files:
            rel_tex = tex_path.relative_to(src_root).as_posix()
            work = td_path / f"build_{tex_path.stem}"
            try:
                shutil.copytree(src_root, work / "src")
                ok, log_text, pdf = await run_latex_two_passes(rel_tex, work / "src", tex_engine, timeout_sec, logger)
                if ok and pdf.exists():
                    out_pdf_paths.append(pdf)
                else:
                    failed.append(rel_tex)
                    log.info("LaTeX FAIL %s\n%s", rel_tex, (log_text or "")[-2000:])
            except Exception:
                # let it crash – log + re-raise (to zobaczy wyższa warstwa/discord.py)
                log.exception("Exception while compiling %s", rel_tex)
                raise

        # 4) Spakuj wyjściowe PDF-y
        out_zip = td_path / "compiled_pdfs.zip"
        with zipfile.ZipFile(out_zip, "w", compression=zipfile.ZIP_DEFLATED) as zf:
            for pdf in out_pdf_paths:
                zf.write(pdf, arcname=pdf.name)

        return out_zip.read_bytes(), failed
