"""In-memory magazyn widoków wyrenderowanych przez present_data.

Każdy widok jest kluczowany hashem swojej treści (html+js) — tym samym, który
gui_provider wysyła do klienta i który klient odsyła przy nagraniu. Dzięki temu
nagranie zrobione na dowolnej (także starszej) stronie trafia do właściwego widoku.

Podmianka widoku (present_data z parametrem `replaces`) nie nadpisuje starego
wpisu, tylko dokłada nowy i zapamiętuje, co czym zostało zastąpione. Stary widok
musi zostać: nagranie zrobione sekundę przed podmianką jest jeszcze w kolejce
transkrypcji, a jego xpathy odnoszą się do starego drzewa. Nadpisanie treści pod
tym samym hashem rozwiązałoby je na nowym drzewie i po cichu wskazało nie ten
element — dlatego hash pochodzi z treści i nowa treść dostaje nowy hash.
"""

from __future__ import annotations

import hashlib
import threading

_lock = threading.Lock()
_views: dict[str, str] = {}         # hash -> html
_replacements: dict[str, str] = {}  # hash starego widoku -> hash tego, który go zastąpił
_last_hash: str | None = None
_MAX_VIEWS = 500                    # prosty limit, żeby pamięć nie rosła w nieskończoność


def compute_page_hash(html: str, js: str | None = None) -> str:
    """Stabilny identyfikator strony — ta sama treść daje ten sam hash."""
    return hashlib.sha1(f"{html}\x00{js or ''}".encode("utf-8")).hexdigest()


def store(html: str, js: str | None = None, replaces: str | None = None) -> str:
    """Zapisz widok pod jego hashem i oznacz jako ostatni. Zwraca hash.

    `replaces` to hash widoku, który ten widok zastępuje — zapisujemy samo
    powiązanie, treść starego widoku zostaje w magazynie.
    """
    global _last_hash
    page_hash = compute_page_hash(html, js)
    with _lock:
        _views[page_hash] = html
        _last_hash = page_hash
        if replaces and replaces != page_hash:
            _replacements[replaces] = page_hash
            if len(_replacements) > _MAX_VIEWS:
                _replacements.pop(next(iter(_replacements)), None)
        if len(_views) > _MAX_VIEWS:
            oldest = next(iter(_views))
            if oldest != page_hash:
                _views.pop(oldest, None)
    return page_hash


def is_known(page_hash: str) -> bool:
    """Czy taki widok był kiedykolwiek wysłany (i nie wypadł jeszcze z magazynu)."""
    with _lock:
        return page_hash in _views


def current_hash(page_hash: str) -> str:
    """Hash widoku, który dziś stoi w miejscu podanego — po całym łańcuchu podmianek.

    Agent może trzymać hash z promptu, gdy widok został już w tym czasie podmieniony;
    celem podmianki ma być ekran, który użytkownik faktycznie widzi.
    """
    with _lock:
        seen = {page_hash}
        while True:
            nxt = _replacements.get(page_hash)
            # łańcuch może wrócić do punktu wyjścia, gdy agent odtworzył wcześniejszą treść
            if nxt is None or nxt in seen:
                return page_hash
            seen.add(nxt)
            page_hash = nxt


def replaced_by(page_hash: str) -> str | None:
    """Hash, który zastąpił podany widok (None, gdy widok jest nadal aktualny)."""
    latest = current_hash(page_hash)
    return latest if latest != page_hash else None


def get_html(page_hash: str) -> str | None:
    """Pobierz widok po hashu (None, gdy nieznany)."""
    with _lock:
        return _views.get(page_hash)


def get_last_html() -> str | None:
    """Pobierz ostatnio zapisany widok (fallback, gdy brak/nieznany hash)."""
    with _lock:
        return _views.get(_last_hash) if _last_hash else None


def get_last_hash() -> str | None:
    """Hash ostatnio zapisanego widoku — identyfikator tego, co zwraca get_last_html."""
    with _lock:
        return _last_hash if _last_hash and _last_hash in _views else None


# zgodność wstecz: stare wywołania set_last_html nadal działają
def set_last_html(html: str) -> None:
    store(html)
