"""
Kongres PDF import – krok 1: přihlášení, otevření modalu, stažení PDF.
Zápis do MySQL (adeniraImporter.Kongres_pdf_imports) dělá tenhle
script, samotnou analýzu PDF (AI) řeší samostatný navazující script.

Instalace:
    pip install playwright pymysql python-dotenv
    playwright install chromium

Přihlašovací a DB údaje se čtou z .env v téhle složce (viz .env.example) –
nikdy je nedávej natvrdo do kódu, ať se náhodou nedostanou do gitu.

Testování (bez zápisu do DB, s video záznamem běhu pro kontrolu):
    DRY_RUN=true RECORD_VIDEO=true venv/bin/python Kongres_pdf_imports.py
Video (.webm) najdeš v logs/<timestamp>/video/ – stáhni a přehraj lokálně.
Na serveru bez GUI nech HEADLESS=true (default), video vznikne i tak.

Ostrý běh (cron, Linux, headless, každou hodinu):
    0 * * * * /usr/bin/python3 /path/to/Kongres_pdf_imports.py >> /path/to/Kongres_pdf_imports.log 2>&1

Debug: po každém běhu (i chybném) se v LOG_DIR vytvoří trace.zip.
Otevřít přes:
    playwright show-trace logs/<timestamp>/trace.zip
(nebo přetáhnout soubor na https://trace.playwright.dev)
"""

import logging
import os
import re
import sys
import time
from contextlib import contextmanager
from datetime import date, datetime, timedelta

from dotenv import load_dotenv
from playwright.sync_api import sync_playwright

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
load_dotenv(os.path.join(BASE_DIR, ".env"))

# ---------------------------------------------------------------------------
# Konfigurace
# ---------------------------------------------------------------------------
# Stránka je SPA za loginem – neautentizovaný požadavek na TARGET_URL
# zobrazí přihlašovací formulář na stejné adrese, proto LOGIN_URL == TARGET_URL.
TARGET_URL = os.environ.get(
    "KONGRES_TARGET_URL",
    "https://app.buildary.online/projects/38117/diaries/66110/daily-protocols/",
)
LOGIN_URL = os.environ.get("KONGRES_LOGIN_URL", TARGET_URL)
USERNAME = os.environ.get("KONGRES_USER", "TODO")
PASSWORD = os.environ.get("KONGRES_PASS", "TODO")

HEADLESS = os.environ.get("HEADLESS", "true").lower() != "false"
SLOW_MO_MS = int(os.environ.get("SLOW_MO_MS", "0"))
DRY_RUN = os.environ.get("DRY_RUN", "false").lower() == "true"
RECORD_VIDEO = os.environ.get("RECORD_VIDEO", "false").lower() == "true"

DOWNLOAD_DIR = os.environ.get("DOWNLOAD_DIR", os.path.join(BASE_DIR, "PDF"))
LOG_DIR = os.path.join(BASE_DIR, "logs", datetime.now().strftime("%Y%m%d_%H%M%S"))

DB_CONFIG = {
    "host": os.environ.get("KONGRES_DB_HOST", "localhost"),
    "user": os.environ.get("KONGRES_DB_USER", "TODO"),
    "password": os.environ.get("KONGRES_DB_PASS", "TODO"),
    "database": "adeniraImporter",
    "charset": "utf8mb4",
}

LOCK_FILE = os.path.join(BASE_DIR, "download_pdf.lock")

# ---------------------------------------------------------------------------
# Logging – konzole + soubor, jeden log na běh
# ---------------------------------------------------------------------------
os.makedirs(LOG_DIR, exist_ok=True)
logger = logging.getLogger("kongres_download")
logger.setLevel(logging.DEBUG)

_fmt = logging.Formatter("%(asctime)s [%(levelname)s] %(message)s", "%Y-%m-%d %H:%M:%S")

_console = logging.StreamHandler(sys.stdout)
_console.setFormatter(_fmt)
logger.addHandler(_console)

_file = logging.FileHandler(os.path.join(LOG_DIR, "run.log"), encoding="utf-8")
_file.setFormatter(_fmt)
logger.addHandler(_file)


@contextmanager
def step(name: str, page=None):
    """Zaloguje začátek/konec/dobu trvání kroku; při chybě uloží screenshot."""
    logger.info(f"→ {name}")
    start = time.monotonic()
    try:
        yield
        logger.info(f"✓ {name} ({time.monotonic() - start:.2f}s)")
    except Exception:
        logger.exception(f"✗ {name} SELHALO")
        if page is not None:
            shot_path = os.path.join(LOG_DIR, f"error_{int(time.time())}.png")
            try:
                page.screenshot(path=shot_path, full_page=True)
                logger.error(f"Screenshot chyby uložen: {shot_path}")
            except Exception:
                logger.exception("Nepodařilo se uložit screenshot chyby")
        raise


# ---------------------------------------------------------------------------
# Jednoduchý lock proti souběžnému běhu
# ---------------------------------------------------------------------------
@contextmanager
def single_instance_lock():
    if os.path.exists(LOCK_FILE):
        age_min = (time.time() - os.path.getmtime(LOCK_FILE)) / 60
        raise RuntimeError(
            f"Lock soubor už existuje ({LOCK_FILE}, stáří {age_min:.1f} min) – "
            "buď předchozí běh ještě neskončil, nebo spadl bez úklidu. Zkontroluj a smaž ručně."
        )
    with open(LOCK_FILE, "w") as f:
        f.write(str(os.getpid()))
    try:
        yield
    finally:
        os.remove(LOCK_FILE)


# ---------------------------------------------------------------------------
# Kalendář dnů – výběr, co se má stahovat
#
# Dlaždice dne má tři možné stavy (podle CSS třídy na vnitřním elementu):
#   - "...locked_<hash>"                    -> zamknuté, historické (uzavřeno)
#   - "...not-locked_<hash>"                -> odemčené, obsah se ještě může měnit
#   - "...day-button--disabled_<hash>"      -> budoucí den, žádný obsah
#
# Ty hashe za podtržítkem jsou CSS-modules hash a mohou se změnit při
# redeployi Buildary aplikace, proto se necílí přesným selektorem, ale
# hledáním podřetězce v HTML dlaždice.
# ---------------------------------------------------------------------------
def parse_tile_date(date_text: str, today: date) -> date:
    """'ÚT 11.8.' / '10.8.' -> date(...). Řeší přechod roku (běh v prosinci vidí den v lednu)."""
    match = re.search(r"(\d{1,2})\.\s*(\d{1,2})\.", date_text)
    if not match:
        raise ValueError(f"Nepodařilo se vyparsovat datum z textu dlaždice: {date_text!r}")
    day_num, month_num = int(match.group(1)), int(match.group(2))
    candidate = date(today.year, month_num, day_num)
    if (today - candidate).days > 180:
        candidate = date(today.year + 1, month_num, day_num)
    return candidate


# Kalendář je stránkovaný po týdnech (Po-Ne) – tlačítka "<"/">" mají obě
# stejnou třídu, rozlišují se jen ikonou uvnitř. Třída má opět CSS-modules
# hash suffix, proto substring match jen na stabilní část názvu.
PREV_WEEK_BUTTON = '[class*="daily-calendar-module-move-button"]:has(svg[data-icon="chevron-left"])'
NEXT_WEEK_BUTTON = '[class*="daily-calendar-module-move-button"]:has(svg[data-icon="chevron-right"])'


def get_day_tiles(page) -> list[dict]:
    # ":visible" je nutné – stránka má vedle desktopového kalendáře i skrytou
    # mobilní/responsive variantu se stejnou třídou (jen jinak formátovaný
    # date_text, např. "Ne 16.8.2026" místo "NE 16.8."). Bez filtru se tahle
    # neviditelná duplicitní dlaždice dostane do výběru a klik na ni pak
    # 30s timeoutuje ("element is not visible") – ověřeno na produkci 19.8.2026.
    wraps = page.query_selector_all('[class*="daily-calendar-module-day-button-wrap"]:visible')
    tiles = []
    for wrap in wraps:
        html = wrap.inner_html()
        if "day-button--disabled" in html:
            status = "disabled"
        elif "not-locked" in html:
            status = "not_locked"
        elif "locked" in html:
            status = "locked"
        elif "empty" in html:
            # Den existuje, ale nemá žádný obsah (žádné PDF ke stažení).
            status = "empty"
        else:
            status = "unknown"
        date_el = wrap.query_selector('[class*="day-button__date"]')
        date_text = date_el.inner_text().strip() if date_el else None
        tiles.append({"wrap": wrap, "status": status, "date_text": date_text})
    return tiles


def switch_week(page, direction: str) -> None:
    """Přepne kalendář na sousední týden (direction: 'prev'/'next').

    Přepnutí je čistě klientská (SPA) operace bez síťového volání, takže
    "networkidle" doběhne prakticky okamžitě – ještě předtím, než se stránka
    stihne reálně překreslit. Malá pevná pauza navíc snižuje riziko, že se
    dlaždice přečtou v půli překreslení.
    """
    page.click(PREV_WEEK_BUTTON if direction == "prev" else NEXT_WEEK_BUTTON)
    page.wait_for_load_state("networkidle")
    page.wait_for_timeout(300)


def scan_week(page, label: str) -> list[dict]:
    """Načte dlaždice aktuálně zobrazeného týdne a zaloguje jejich stavy.

    Vrácené 'wrap' handly jsou platné jen do příštího přepnutí týdne (SPA
    zobrazení dlaždice zruší) – pro klikání se proto vždy znovu dohledávají
    čerstvě přes get_day_tiles() až na správném týdnu, ne z tohohle skenu.
    """
    tiles = get_day_tiles(page)
    logger.debug(
        f"Stavy dlaždic ({label}): "
        + ", ".join(f"{t['date_text']!r}={t['status']}" for t in tiles)
    )
    return tiles


def select_days_to_process(tiles: list[dict]) -> list[dict]:
    """Poslední 'locked' den + všechny následující 'not_locked'/'empty' dny (do prvního 'disabled').

    'empty' dny se v okně nechávají (aby nezastavily hledání dalších not_locked
    dnů za nimi), ale při stahování se níž přeskočí – nemají žádné PDF.

    'tiles' může být spojení dlaždic z více týdnů za sebou (viz run_download) –
    poslední 'locked' den totiž nemusí být ve stejném týdnu jako dnešek, pokud
    kalendář ukazuje jen aktuální týden a poslední uzamčený den je starší.
    """
    last_locked_idx = None
    for i, tile in enumerate(tiles):
        if tile["status"] == "locked":
            last_locked_idx = i
    if last_locked_idx is None:
        return []

    selected = [tiles[last_locked_idx]]
    for tile in tiles[last_locked_idx + 1:]:
        if tile["status"] not in ("not_locked", "empty"):
            break
        selected.append(tile)
    return selected


# ---------------------------------------------------------------------------
# Playwright flow
# ---------------------------------------------------------------------------
def run_download() -> list:
    os.makedirs(DOWNLOAD_DIR, exist_ok=True)

    with sync_playwright() as playwright:
        browser = playwright.chromium.launch(headless=HEADLESS, slow_mo=SLOW_MO_MS)
        context = browser.new_context(
            accept_downloads=True,
            record_video_dir=os.path.join(LOG_DIR, "video") if RECORD_VIDEO else None,
            record_video_size={"width": 1280, "height": 720} if RECORD_VIDEO else None,
        )
        context.tracing.start(screenshots=True, snapshots=True, sources=True)
        page = context.new_page()

        # Zrcadlí console.log/console.error ze stránky do našeho logu – užitečné při ladění
        page.on("console", lambda msg: logger.debug(f"[browser console:{msg.type}] {msg.text}"))
        page.on("pageerror", lambda err: logger.warning(f"[browser page error] {err}"))

        downloaded = []  # list[(diary_date, pdf_path)]
        try:
            with step("Otevření login stránky", page):
                page.goto(LOGIN_URL, wait_until="domcontentloaded")

            with step("Vyplnění a odeslání přihlašovacího formuláře", page):
                page.fill("input[name='email']", USERNAME)
                page.fill("input[name='password']", PASSWORD)
                # Na stránce jsme neměli k dispozici HTML submit tlačítka –
                # Enter v poli hesla je nejběžnější způsob odeslání SPA loginu.
                # TODO: pokud login neproběhne, ověř přes trace.zip a nahraď
                # přesným selektorem submit tlačítka.
                page.press("input[name='password']", "Enter")
                page.wait_for_load_state("networkidle")

            with step("Přechod na cílovou stránku (denní protokol)", page):
                page.goto(TARGET_URL, wait_until="domcontentloaded")

            with step("Čekání na vykreslení kalendáře dnů", page):
                # SPA – kalendář se naplní daty až po doběhnutí API volání,
                # domcontentloaded na to nestačí.
                page.wait_for_selector('[class*="daily-calendar-module-day-button-wrap"]', timeout=20000)

            # Kalendář po načtení ukazuje jen aktuální týden. Poslední 'locked'
            # den ale může ležet v předchozím týdnu (cron běží denně, takže
            # o víc než týden pozadu by se dostat neměl) – proto se kromě
            # aktuálního týdne načte i ten předchozí a hledá se v obou.
            today = date.today()
            with step("Přechod na předchozí týden", page):
                switch_week(page, "prev")
            with step("Načtení předchozího týdne", page):
                prev_week_tiles = scan_week(page, "minulý týden")

            with step("Návrat na aktuální týden", page):
                switch_week(page, "next")
            with step("Načtení aktuálního týdne", page):
                curr_week_tiles = scan_week(page, "aktuální týden")
            current_week_view = "curr"  # po návratu jsme zpět na aktuálním týdnu

            with step("Výběr dnů ke stažení", page):
                selected = select_days_to_process(prev_week_tiles + curr_week_tiles)
                logger.info(
                    f"Vybráno ke stažení: {len(selected)} "
                    f"({', '.join(t['date_text'] for t in selected) or '-'})"
                )
                if not selected:
                    logger.warning(
                        "V aktuálním ani předchozím týdnu nebyl nalezen žádný "
                        "'locked' den – nic se nestahuje. Pokud tohle přetrvává "
                        "víc dní, zkontroluj ručně stav na Buildary (může to "
                        "znamenat delší výpadek nebo změnu na jejich straně)."
                    )

            for tile_meta in selected:
                diary_date = parse_tile_date(tile_meta["date_text"], today)
                label = diary_date.isoformat()

                if tile_meta["status"] == "empty":
                    logger.info(f"Den {label} je prázdný (bez obsahu) – přeskočeno, nic ke stažení.")
                    record_empty_day(diary_date)
                    continue

                # Dlaždice pochází z dřívějšího skenu (možná jiného týdne) a
                # její 'wrap' handle je po přepínání týdnů neplatný – přepni
                # na správný týden a dlaždici podle textu data dohledej znovu.
                day_monday = diary_date - timedelta(days=diary_date.weekday())
                today_monday = today - timedelta(days=today.weekday())
                target_week_view = "prev" if day_monday < today_monday else "curr"
                if target_week_view != current_week_view:
                    with step(f"Přepnutí na týden dne {label}", page):
                        switch_week(page, "prev" if target_week_view == "prev" else "next")
                        current_week_view = target_week_view

                with step(f"Výběr dne {label} v kalendáři", page):
                    fresh_tiles = get_day_tiles(page)
                    tile = next((t for t in fresh_tiles if t["date_text"] == tile_meta["date_text"]), None)
                    if tile is None:
                        logger.error(
                            "Dlaždice pro %s (%r) nenalezena po přepnutí na týden '%s'. "
                            "Aktuálně viditelné dlaždice: %s",
                            label, tile_meta["date_text"], current_week_view,
                            ", ".join(f"{t['date_text']!r}={t['status']}" for t in fresh_tiles) or "(žádné)",
                        )
                        raise RuntimeError(
                            f"Dlaždice pro {label} ({tile_meta['date_text']!r}) "
                            f"nenalezena po přepnutí na týden '{current_week_view}'."
                        )
                    tile["wrap"].click()
                    page.wait_for_load_state("networkidle")

                with step(f"Otevření náhledu PDF ({label})", page):
                    # Tlačítko má na stránce i skrytou responsive variantu
                    # (mobilní layout) se stejným textem – ":visible" zajistí,
                    # že se klikne jen na tu skutečně zobrazenou.
                    page.click("button:has-text('Náhled PDF'):visible")

                with step(f"Kliknutí na stažení a uložení PDF ({label})", page):
                    with page.expect_download() as download_info:
                        # Tlačítko je jen ikona (text je vizuálně skrytý), proto
                        # cílíme na ikonu "download" uvnitř tlačítka.
                        page.click("button:has(svg[data-icon='download']):visible")
                    download = download_info.value
                    pdf_path = os.path.join(DOWNLOAD_DIR, f"kongres_{label}.pdf")
                    download.save_as(pdf_path)
                    logger.info(f"PDF uloženo do: {pdf_path}")
                    downloaded.append((diary_date, pdf_path))

                with step(f"Zavření náhledu PDF ({label})", page):
                    # Modal s náhledem zůstává otevřený a jeho overlay by
                    # blokoval klik na dlaždici dalšího dne – Escape ho zavře.
                    page.keyboard.press("Escape")
                    page.wait_for_timeout(500)

            # Best-effort – PDF už jsou stažená, takže selhání odhlášení
            # nesmí shodit celý běh, jen se zaloguje jako varování.
            try:
                with step("Odhlášení z účtu", page):
                    page.click("button.navbar-button:has(svg[data-icon='user']):visible")
                    page.click("div.dropdown-item:has-text('Odhlásit'):visible")
                    page.wait_for_url("**firstis.eu**", timeout=15000)
                    page.click("button.button--danger:has-text('Ano'):visible")
                    page.wait_for_load_state("networkidle")
            except Exception:
                logger.warning("Odhlášení se nezdařilo (nekritické, PDF jsou už stažená) – pokračuji.")

        finally:
            trace_path = os.path.join(LOG_DIR, "trace.zip")
            context.tracing.stop(path=trace_path)
            logger.info(f"Playwright trace uložen: {trace_path} (prohlédni: playwright show-trace {trace_path})")

            video_path = page.video.path() if (RECORD_VIDEO and page.video) else None
            context.close()
            browser.close()
            if video_path:
                logger.info(f"Video záznam uložen: {video_path}")

        return downloaded


# ---------------------------------------------------------------------------
# Zápis do DB
# ---------------------------------------------------------------------------
def record_in_db(downloaded: list) -> None:
    if DRY_RUN:
        for diary_date, file_path in downloaded:
            logger.info(f"[DRY_RUN] Přeskakuji zápis do DB pro {diary_date.isoformat()}: {file_path}")
        return

    import pymysql

    with step(f"Zápis {len(downloaded)} záznamů do adeniraImporter.Kongres_pdf_imports"):
        conn = pymysql.connect(**DB_CONFIG)
        try:
            with conn.cursor() as cur:
                for diary_date, file_path in downloaded:
                    cur.execute(
                        """
                        INSERT INTO Kongres_pdf_imports
                            (source, diary_date, file_path, downloaded_at, status)
                        VALUES (%s, %s, %s, %s, 'pending')
                        """,
                        ("kongres", diary_date, file_path, datetime.now()),
                    )
            conn.commit()
        finally:
            conn.close()


def record_empty_day(diary_date) -> None:
    """'empty' den nemá žádné PDF ke stažení, ale i tak by měl figurovat
    v Kongres_diary_days jako otevřený den (např. dnešek), ať se objeví
    v denním emailovém reportu. Zapisuje se přímo sem, mimo Kongres_pdf_imports
    (tam patří jen skutečně stažené soubory)."""
    if DRY_RUN:
        logger.info(f"[DRY_RUN] Přeskakuji zápis prázdného dne do DB: {diary_date.isoformat()}")
        return

    import pymysql

    with step(f"Zápis prázdného dne {diary_date.isoformat()} do Kongres_diary_days"):
        conn = pymysql.connect(**DB_CONFIG)
        try:
            with conn.cursor() as cur:
                cur.execute(
                    """
                    INSERT INTO Kongres_diary_days (diary_date, status, last_checked_at)
                    VALUES (%s, 'open', %s)
                    ON DUPLICATE KEY UPDATE last_checked_at = VALUES(last_checked_at)
                    """,
                    (diary_date, datetime.now()),
                )
            conn.commit()
        finally:
            conn.close()


def main() -> None:
    logger.info(f"=== Start běhu (HEADLESS={HEADLESS}, DRY_RUN={DRY_RUN}) ===")
    try:
        with single_instance_lock():
            downloaded = run_download()
            record_in_db(downloaded)
        logger.info(f"=== Běh úspěšně dokončen, stažených dnů: {len(downloaded)} ===")
    except Exception as exc:
        logger.error(f"=== Běh SKONČIL CHYBOU: {exc} ===")
        sys.exit(1)


if __name__ == "__main__":
    main()
