"""
Jednoduchý lokální správce hesel - verze v20.

Instalace:
    py -m pip install --upgrade cryptography ttkbootstrap pyperclip

Spuštění:
    py password_manager_v17.py

Změny v20:
    - V nastavení lze změnit barevné schéma aplikace.
    - Přidána další barevná schémata včetně růžové.
    - Tlačítko pro přidání záznamu je nově pojmenované Uložit.
    - Import hesel z CSV exportu prohlížeče Chrome.

Změny v16:
    - Přihlašovací obrazovka sladěná do petrolejových odstínů.
    - Výraznější akcentní tlačítko pro odemknutí trezoru.
    - Při založení nového trezoru se hlavní heslo zadává dvakrát.
    - Silnější parametry Argon2id pro moderní zařízení.
    - Generované heslo vždy obsahuje velké písmeno, malé písmeno, číslici a speciální znak.
    - Automatické uzamčení aplikace při nečinnosti.
    - Možnost změny hlavního hesla s přešifrováním trezoru novou solí.
"""

import base64
import binascii
import csv
import json
import os
import secrets
import string
import uuid
import importlib.util
from dataclasses import dataclass, asdict, field
from pathlib import Path
from typing import Any

import pyperclip
import ttkbootstrap as tb
import tkinter as tk
import tkinter.filedialog as filedialog
import tkinter.messagebox as system_messagebox
from tkinter import TclError
from ttkbootstrap.constants import *
from ttkbootstrap.dialogs import Messagebox


APP_DIR = Path.home() / ".simple_password_manager"
VAULT_FILE = APP_DIR / "vault.dat"
TEMP_VAULT_FILE = APP_DIR / "vault.dat.tmp"
SALT_FILE = APP_DIR / "salt.bin"
TEMP_SALT_FILE = APP_DIR / "salt.bin.tmp"
SETTINGS_FILE = APP_DIR / "settings.json"
APP_ICON_FILE = Path(__file__).with_name("trezor_logo.png")
APP_ICON_BITMAP_FILE = Path(__file__).with_name("trezor_logo.ico")
VAULT_FILE_FORMAT_VERSION = 2

# Aktuální Argon2id parametry jsou záměrně svižnější pro lokální desktopovou appku.
# Starší trezory se parametry 256 MiB / 5 iterací při odemčení načtou a uloží nově.
ARGON2_ITERATIONS = 3
ARGON2_LANES = 4
ARGON2_MEMORY_COST = 64 * 1024
ARGON2_KEY_LENGTH = 32
LEGACY_ARGON2_ITERATIONS = 5
LEGACY_ARGON2_MEMORY_COST = 256 * 1024

CLIPBOARD_CLEAR_SECONDS = 20
AUTO_LOCK_SECONDS = 5 * 60

DEFAULT_COLOR_SCHEME = "petroleum"
DEFAULT_LANGUAGE = "cs"

LANGUAGES = {
    "cs": "Čeština",
    "sk": "Slovenčina",
    "en": "English",
    "es": "Español",
    "de": "Deutsch",
}

TRANSLATIONS = {
    "cs": {
        "app_title": "Správce hesel v20",
        "unlock_subtitle": "Odemkni trezor hlavním heslem",
        "create_subtitle": "Vytvoř nový trezor zadáním hlavního hesla",
        "master_password": "HLAVNÍ HESLO",
        "confirm_master_password": "POTVRZENÍ HLAVNÍHO HESLA",
        "unlock_vault": "Odemknout trezor",
        "create_vault": "Vytvořit trezor",
        "login_hint": "Hlavní heslo se nikam neukládá. Slouží k lokálnímu dešifrování vaší databáze přímo v tomto zařízení.",
        "create_hint": "Pozor: bez hlavního hesla trezor nepůjde obnovit.",
        "new_entry": "+  Nový záznam",
        "import_chrome": "Import z Chrome",
        "local_vault": "Lokální trezor",
        "settings": "Nastavení",
        "lock": "Uzamknout",
        "all_records": "Všechny záznamy",
        "all": "Vše",
        "favorite": "Oblíbené",
        "work": "Práce",
        "personal": "Osobní",
        "untitled": "Bez názvu",
        "no_user": "Bez uživatele",
        "missing_record": "Vybraný záznam už neexistuje.",
        "record_not_found": "Záznam nenalezen",
        "new_record": "Nový záznam",
        "created_now": "Vytvořeno před chvílí",
        "created_old": "Vytvořeno před 3 měsíci",
        "name": "NÁZEV",
        "user_email": "UŽIVATEL / E-MAIL",
        "tag": "ŠTÍTEK",
        "password": "HESLO",
        "show": "Ukázat",
        "copy_short": "Kop.",
        "generate": "Generovat",
        "note": "POZNÁMKA",
        "delete": "Smazat",
        "cancel": "Zrušit",
        "save": "Uložit",
        "save_changes": "Uložit změny",
        "yes": "Ano",
        "no": "Ne",
        "color_scheme": "Barevné schéma",
        "language": "Jazyk",
        "done": "Hotovo",
        "settings_saved": "Nastavení bylo uloženo.",
        "change_master_password": "Změna hlavního hesla",
        "old_master_password": "Původní hlavní heslo",
        "new_master_password": "Nové hlavní heslo",
        "new_master_password_again": "Nové hlavní heslo znovu",
        "change_password": "Změnit heslo",
        "no_records": "Žádné záznamy",
        "add_first_record": "Přidejte první heslo tlačítkem +.",
        "confirm_import": "Potvrzení importu",
        "confirm_delete": "Potvrzení smazání",
        "record_count": "{count} záznamů",
        "import_confirm_message": "Importovat {count} záznamů z Chrome?\n\nPřeskočeno bez hesla nebo URL: {skipped}\nPřeskočeno jako duplicita: {duplicates}",
        "delete_confirm_message": "Opravdu chceš záznam smazat?",
        "strength_0": "Síla hesla: žádné",
        "strength_1": "Síla hesla: slabé",
        "strength_2": "Síla hesla: střední",
        "strength_3": "Síla hesla: silné",
        "strength_4": "Síla hesla: velmi silné",
        "enter_old_password": "Zadej původní hlavní heslo.",
        "missing_password_title": "Chybí heslo",
        "missing_title": "Vyplň název záznamu.",
        "missing_title_title": "Chybí název",
        "missing_entry_password": "Vyplň nebo vygeneruj heslo.",
        "entry_saved": "Záznam byl uložen.",
        "changes_saved": "Změny byly uloženy.",
        "select_record_first": "Nejdřív vyber záznam.",
        "no_record_selected": "Není vybrán záznam",
        "entry_deleted": "Záznam byl smazán.",
        "nothing_to_copy": "Není co kopírovat.",
    },
    "sk": {
        "app_title": "Správca hesiel v20",
        "unlock_subtitle": "Odomkni trezor hlavným heslom",
        "create_subtitle": "Vytvor nový trezor zadaním hlavného hesla",
        "master_password": "HLAVNÉ HESLO",
        "confirm_master_password": "POTVRDENIE HLAVNÉHO HESLA",
        "unlock_vault": "Odomknúť trezor",
        "create_vault": "Vytvoriť trezor",
        "login_hint": "Hlavné heslo sa nikam neukladá. Slúži na lokálne dešifrovanie vašej databázy priamo v tomto zariadení.",
        "create_hint": "Pozor: bez hlavného hesla trezor nebude možné obnoviť.",
        "new_entry": "+  Nový záznam",
        "import_chrome": "Import z Chrome",
        "local_vault": "Lokálny trezor",
        "settings": "Nastavenia",
        "lock": "Uzamknúť",
        "all_records": "Všetky záznamy",
        "all": "Všetko",
        "favorite": "Obľúbené",
        "work": "Práca",
        "personal": "Osobné",
        "untitled": "Bez názvu",
        "no_user": "Bez používateľa",
        "missing_record": "Vybraný záznam už neexistuje.",
        "record_not_found": "Záznam nenájdený",
        "new_record": "Nový záznam",
        "created_now": "Vytvorené pred chvíľou",
        "created_old": "Vytvorené pred 3 mesiacmi",
        "name": "NÁZOV",
        "user_email": "POUŽÍVATEĽ / E-MAIL",
        "tag": "ŠTÍTOK",
        "password": "HESLO",
        "show": "Ukázať",
        "copy_short": "Kop.",
        "generate": "Generovať",
        "note": "POZNÁMKA",
        "delete": "Vymazať",
        "cancel": "Zrušiť",
        "save": "Uložiť",
        "save_changes": "Uložiť zmeny",
        "yes": "Áno",
        "no": "Nie",
        "color_scheme": "Farebná schéma",
        "language": "Jazyk",
        "done": "Hotovo",
        "settings_saved": "Nastavenia boli uložené.",
        "change_master_password": "Zmena hlavného hesla",
        "old_master_password": "Pôvodné hlavné heslo",
        "new_master_password": "Nové hlavné heslo",
        "new_master_password_again": "Nové hlavné heslo znova",
        "change_password": "Zmeniť heslo",
        "no_records": "Žiadne záznamy",
        "add_first_record": "Pridajte prvé heslo tlačidlom +.",
        "confirm_import": "Potvrdenie importu",
        "confirm_delete": "Potvrdenie vymazania",
        "record_count": "{count} záznamov",
        "import_confirm_message": "Importovať {count} záznamov z Chrome?\n\nPreskočené bez hesla alebo URL: {skipped}\nPreskočené ako duplicita: {duplicates}",
        "delete_confirm_message": "Naozaj chceš záznam vymazať?",
        "strength_0": "Sila hesla: žiadna",
        "strength_1": "Sila hesla: slabé",
        "strength_2": "Sila hesla: stredné",
        "strength_3": "Sila hesla: silné",
        "strength_4": "Sila hesla: veľmi silné",
        "enter_old_password": "Zadaj pôvodné hlavné heslo.",
        "missing_password_title": "Chýba heslo",
        "missing_title": "Vyplň názov záznamu.",
        "missing_title_title": "Chýba názov",
        "missing_entry_password": "Vyplň alebo vygeneruj heslo.",
        "entry_saved": "Záznam bol uložený.",
        "changes_saved": "Zmeny boli uložené.",
        "select_record_first": "Najprv vyber záznam.",
        "no_record_selected": "Nie je vybraný záznam",
        "entry_deleted": "Záznam bol vymazaný.",
        "nothing_to_copy": "Nie je čo kopírovať.",
    },
    "en": {
        "app_title": "Password Manager v20",
        "unlock_subtitle": "Unlock your vault with the master password",
        "create_subtitle": "Create a new vault by entering a master password",
        "master_password": "MASTER PASSWORD",
        "confirm_master_password": "CONFIRM MASTER PASSWORD",
        "unlock_vault": "Unlock vault",
        "create_vault": "Create vault",
        "login_hint": "The master password is never stored. It decrypts your local database directly on this device.",
        "create_hint": "Warning: without the master password, the vault cannot be recovered.",
        "new_entry": "+  New entry",
        "import_chrome": "Import from Chrome",
        "local_vault": "Local vault",
        "settings": "Settings",
        "lock": "Lock",
        "all_records": "All records",
        "all": "All",
        "favorite": "Favorites",
        "work": "Work",
        "personal": "Personal",
        "untitled": "Untitled",
        "no_user": "No user",
        "missing_record": "The selected record no longer exists.",
        "record_not_found": "Record not found",
        "new_record": "New entry",
        "created_now": "Created just now",
        "created_old": "Created 3 months ago",
        "name": "NAME",
        "user_email": "USER / EMAIL",
        "tag": "TAG",
        "password": "PASSWORD",
        "show": "Show",
        "copy_short": "Copy",
        "generate": "Generate",
        "note": "NOTE",
        "delete": "Delete",
        "cancel": "Cancel",
        "save": "Save",
        "save_changes": "Save changes",
        "yes": "Yes",
        "no": "No",
        "color_scheme": "Color scheme",
        "language": "Language",
        "done": "Done",
        "settings_saved": "Settings have been saved.",
        "change_master_password": "Change master password",
        "old_master_password": "Current master password",
        "new_master_password": "New master password",
        "new_master_password_again": "Repeat new master password",
        "change_password": "Change password",
        "no_records": "No records",
        "add_first_record": "Add your first password with the + button.",
        "confirm_import": "Confirm import",
        "confirm_delete": "Confirm delete",
        "record_count": "{count} records",
        "import_confirm_message": "Import {count} records from Chrome?\n\nSkipped without password or URL: {skipped}\nSkipped as duplicates: {duplicates}",
        "delete_confirm_message": "Do you really want to delete this record?",
        "strength_0": "Password strength: none",
        "strength_1": "Password strength: weak",
        "strength_2": "Password strength: fair",
        "strength_3": "Password strength: strong",
        "strength_4": "Password strength: very strong",
        "enter_old_password": "Enter the current master password.",
        "missing_password_title": "Missing password",
        "missing_title": "Fill in the record name.",
        "missing_title_title": "Missing name",
        "missing_entry_password": "Fill in or generate a password.",
        "entry_saved": "Record has been saved.",
        "changes_saved": "Changes have been saved.",
        "select_record_first": "Select a record first.",
        "no_record_selected": "No record selected",
        "entry_deleted": "Record has been deleted.",
        "nothing_to_copy": "Nothing to copy.",
    },
    "es": {
        "app_title": "Gestor de contraseñas v20",
        "unlock_subtitle": "Desbloquea la bóveda con la contraseña maestra",
        "create_subtitle": "Crea una bóveda nueva introduciendo una contraseña maestra",
        "master_password": "CONTRASEÑA MAESTRA",
        "confirm_master_password": "CONFIRMAR CONTRASEÑA MAESTRA",
        "unlock_vault": "Desbloquear bóveda",
        "create_vault": "Crear bóveda",
        "login_hint": "La contraseña maestra no se guarda. Sirve para descifrar localmente la base de datos en este dispositivo.",
        "create_hint": "Aviso: sin la contraseña maestra, la bóveda no se podrá recuperar.",
        "new_entry": "+  Nuevo registro",
        "import_chrome": "Importar de Chrome",
        "local_vault": "Bóveda local",
        "settings": "Configuración",
        "lock": "Bloquear",
        "all_records": "Todos los registros",
        "all": "Todo",
        "favorite": "Favoritos",
        "work": "Trabajo",
        "personal": "Personal",
        "untitled": "Sin título",
        "no_user": "Sin usuario",
        "missing_record": "El registro seleccionado ya no existe.",
        "record_not_found": "Registro no encontrado",
        "new_record": "Nuevo registro",
        "created_now": "Creado hace un momento",
        "created_old": "Creado hace 3 meses",
        "name": "NOMBRE",
        "user_email": "USUARIO / EMAIL",
        "tag": "ETIQUETA",
        "password": "CONTRASEÑA",
        "show": "Mostrar",
        "copy_short": "Copiar",
        "generate": "Generar",
        "note": "NOTA",
        "delete": "Eliminar",
        "cancel": "Cancelar",
        "save": "Guardar",
        "save_changes": "Guardar cambios",
        "yes": "Sí",
        "no": "No",
        "color_scheme": "Esquema de color",
        "language": "Idioma",
        "done": "Listo",
        "settings_saved": "La configuración se ha guardado.",
        "change_master_password": "Cambiar contraseña maestra",
        "old_master_password": "Contraseña maestra actual",
        "new_master_password": "Nueva contraseña maestra",
        "new_master_password_again": "Repetir nueva contraseña maestra",
        "change_password": "Cambiar contraseña",
        "no_records": "No hay registros",
        "add_first_record": "Añade tu primera contraseña con el botón +.",
        "confirm_import": "Confirmar importación",
        "confirm_delete": "Confirmar eliminación",
        "record_count": "{count} registros",
        "import_confirm_message": "¿Importar {count} registros de Chrome?\n\nOmitidos sin contraseña o URL: {skipped}\nOmitidos como duplicados: {duplicates}",
        "delete_confirm_message": "¿Seguro que quieres eliminar este registro?",
        "strength_0": "Seguridad de la contraseña: ninguna",
        "strength_1": "Seguridad de la contraseña: débil",
        "strength_2": "Seguridad de la contraseña: media",
        "strength_3": "Seguridad de la contraseña: fuerte",
        "strength_4": "Seguridad de la contraseña: muy fuerte",
        "enter_old_password": "Introduce la contraseña maestra actual.",
        "missing_password_title": "Falta la contraseña",
        "missing_title": "Rellena el nombre del registro.",
        "missing_title_title": "Falta el nombre",
        "missing_entry_password": "Rellena o genera una contraseña.",
        "entry_saved": "El registro se ha guardado.",
        "changes_saved": "Los cambios se han guardado.",
        "select_record_first": "Selecciona primero un registro.",
        "no_record_selected": "No hay ningún registro seleccionado",
        "entry_deleted": "El registro se ha eliminado.",
        "nothing_to_copy": "No hay nada que copiar.",
    },
    "de": {
        "app_title": "Passwortmanager v20",
        "unlock_subtitle": "Tresor mit dem Master-Passwort entsperren",
        "create_subtitle": "Neuen Tresor mit einem Master-Passwort erstellen",
        "master_password": "MASTER-PASSWORT",
        "confirm_master_password": "MASTER-PASSWORT BESTÄTIGEN",
        "unlock_vault": "Tresor entsperren",
        "create_vault": "Tresor erstellen",
        "login_hint": "Das Master-Passwort wird nicht gespeichert. Es entschlüsselt die lokale Datenbank direkt auf diesem Gerät.",
        "create_hint": "Achtung: Ohne Master-Passwort kann der Tresor nicht wiederhergestellt werden.",
        "new_entry": "+  Neuer Eintrag",
        "import_chrome": "Aus Chrome importieren",
        "local_vault": "Lokaler Tresor",
        "settings": "Einstellungen",
        "lock": "Sperren",
        "all_records": "Alle Einträge",
        "all": "Alle",
        "favorite": "Favoriten",
        "work": "Arbeit",
        "personal": "Privat",
        "untitled": "Ohne Titel",
        "no_user": "Kein Benutzer",
        "missing_record": "Der ausgewählte Eintrag existiert nicht mehr.",
        "record_not_found": "Eintrag nicht gefunden",
        "new_record": "Neuer Eintrag",
        "created_now": "Gerade erstellt",
        "created_old": "Vor 3 Monaten erstellt",
        "name": "NAME",
        "user_email": "BENUTZER / E-MAIL",
        "tag": "TAG",
        "password": "PASSWORT",
        "show": "Anzeigen",
        "copy_short": "Kopieren",
        "generate": "Generieren",
        "note": "NOTIZ",
        "delete": "Löschen",
        "cancel": "Abbrechen",
        "save": "Speichern",
        "save_changes": "Änderungen speichern",
        "yes": "Ja",
        "no": "Nein",
        "color_scheme": "Farbschema",
        "language": "Sprache",
        "done": "Fertig",
        "settings_saved": "Die Einstellungen wurden gespeichert.",
        "change_master_password": "Master-Passwort ändern",
        "old_master_password": "Aktuelles Master-Passwort",
        "new_master_password": "Neues Master-Passwort",
        "new_master_password_again": "Neues Master-Passwort wiederholen",
        "change_password": "Passwort ändern",
        "no_records": "Keine Einträge",
        "add_first_record": "Füge dein erstes Passwort mit der +-Schaltfläche hinzu.",
        "confirm_import": "Import bestätigen",
        "confirm_delete": "Löschen bestätigen",
        "record_count": "{count} Einträge",
        "import_confirm_message": "{count} Einträge aus Chrome importieren?\n\nOhne Passwort oder URL übersprungen: {skipped}\nAls Duplikate übersprungen: {duplicates}",
        "delete_confirm_message": "Möchtest du diesen Eintrag wirklich löschen?",
        "strength_0": "Passwortstärke: keine",
        "strength_1": "Passwortstärke: schwach",
        "strength_2": "Passwortstärke: mittel",
        "strength_3": "Passwortstärke: stark",
        "strength_4": "Passwortstärke: sehr stark",
        "enter_old_password": "Gib das aktuelle Master-Passwort ein.",
        "missing_password_title": "Passwort fehlt",
        "missing_title": "Gib den Namen des Eintrags ein.",
        "missing_title_title": "Name fehlt",
        "missing_entry_password": "Gib ein Passwort ein oder generiere eines.",
        "entry_saved": "Der Eintrag wurde gespeichert.",
        "changes_saved": "Die Änderungen wurden gespeichert.",
        "select_record_first": "Wähle zuerst einen Eintrag aus.",
        "no_record_selected": "Kein Eintrag ausgewählt",
        "entry_deleted": "Der Eintrag wurde gelöscht.",
        "nothing_to_copy": "Es gibt nichts zu kopieren.",
    },
}

CATEGORY_TRANSLATION_KEYS = {
    "Vše": "all",
    "Oblíbené": "favorite",
    "Práce": "work",
    "Osobní": "personal",
}

COLOR_SCHEMES = {
    "petroleum": {
        "name": "Petrolejová",
        "bg": "#123f46",
        "panel": "#123f46",
        "field": "#1d5660",
        "primary": "#0f7f86",
        "primary_hover": "#11959d",
        "accent": "#4fd1d9",
        "text": "#f4fbfb",
        "muted": "#a8cfd2",
        "border": "#4fd1d9",
        "login_card": "#16484f",
        "login_field": "#0f343a",
        "login_button": "#0f8f98",
        "login_button_hover": "#11a7b1",
        "login_button_pressed": "#0b747b",
        "disabled": "#49696d",
    },
    "pink": {
        "name": "Malinová",
        "bg": "#3f1831",
        "panel": "#3f1831",
        "field": "#64264e",
        "primary": "#b93173",
        "primary_hover": "#d04489",
        "accent": "#ff8dc7",
        "text": "#fff7fb",
        "muted": "#f2bfd8",
        "border": "#ff8dc7",
        "login_card": "#4d1d3b",
        "login_field": "#321227",
        "login_button": "#c83a7c",
        "login_button_hover": "#e04b92",
        "login_button_pressed": "#99265f",
        "disabled": "#75586a",
    },
    "forest": {
        "name": "Lesní",
        "bg": "#153522",
        "panel": "#153522",
        "field": "#24533a",
        "primary": "#2c8d5a",
        "primary_hover": "#35a96d",
        "accent": "#7ee2a8",
        "text": "#f5fff8",
        "muted": "#b6d9c3",
        "border": "#7ee2a8",
        "login_card": "#1b422b",
        "login_field": "#102919",
        "login_button": "#2f965e",
        "login_button_hover": "#39b472",
        "login_button_pressed": "#237448",
        "disabled": "#526b5b",
    },
    "midnight": {
        "name": "Noční modrá",
        "bg": "#17213b",
        "panel": "#17213b",
        "field": "#26375f",
        "primary": "#4267b2",
        "primary_hover": "#557bd1",
        "accent": "#8ab4ff",
        "text": "#f6f9ff",
        "muted": "#bac8e6",
        "border": "#8ab4ff",
        "login_card": "#1f2c4d",
        "login_field": "#11192e",
        "login_button": "#466fbd",
        "login_button_hover": "#5a85dc",
        "login_button_pressed": "#345494",
        "disabled": "#55627d",
    },
}


@dataclass
class PasswordEntry:
    title: str
    username: str
    password: str
    url: str = ""
    note: str = ""
    category: str = "Osobní"
    id: str = field(default_factory=lambda: str(uuid.uuid4()))


class Vault:
    def __init__(self) -> None:
        APP_DIR.mkdir(parents=True, exist_ok=True)
        self.entries: list[PasswordEntry] = []
        self.fernet: Any | None = None
        self.salt: bytes | None = None
        self.derived_key: bytes | None = None

    def _crypto_dependencies(self):
        try:
            from cryptography.fernet import Fernet, InvalidToken
            from cryptography.hazmat.primitives.kdf.argon2 import Argon2id
        except ImportError as exc:
            raise RuntimeError(
                "Knihovna cryptography nemá dostupný Argon2id. "
                "Spusť: py -m pip install --upgrade cryptography"
            ) from exc

        return Fernet, InvalidToken, Argon2id

    def _load_or_create_salt(self) -> bytes:
        if self.salt is not None:
            return self.salt

        vault_salt = self._read_vault_salt()
        if vault_salt is not None:
            self.salt = vault_salt
            return self.salt

        if SALT_FILE.exists():
            self.salt = SALT_FILE.read_bytes()
            return self.salt

        self.salt = os.urandom(16)
        return self.salt

    def _read_vault_file(self) -> tuple[bytes, bytes | None]:
        vault_data = VAULT_FILE.read_bytes()

        try:
            envelope = json.loads(vault_data.decode("utf-8"))
        except (UnicodeDecodeError, json.JSONDecodeError):
            return vault_data, None

        if not isinstance(envelope, dict) or envelope.get("version") != VAULT_FILE_FORMAT_VERSION:
            return vault_data, None

        try:
            salt = base64.b64decode(envelope["salt"])
            encrypted_data = base64.b64decode(envelope["payload"])
        except (KeyError, TypeError, ValueError, binascii.Error):
            return vault_data, None

        return encrypted_data, salt

    def _read_vault_salt(self) -> bytes | None:
        if not VAULT_FILE.exists():
            return None

        _encrypted_data, vault_salt = self._read_vault_file()
        return vault_salt

    def _write_salt_atomic(self, salt: bytes) -> None:
        with open(TEMP_SALT_FILE, "wb") as temp_file:
            temp_file.write(salt)
            temp_file.flush()
            os.fsync(temp_file.fileno())

        os.replace(TEMP_SALT_FILE, SALT_FILE)

    def _derive_key(
        self,
        master_password: str,
        salt: bytes | None = None,
        iterations: int = ARGON2_ITERATIONS,
        memory_cost: int = ARGON2_MEMORY_COST,
    ) -> bytes:
        _fernet_class, _invalid_token_class, argon2id_class = self._crypto_dependencies()
        used_salt = salt if salt is not None else self._load_or_create_salt()

        kdf = argon2id_class(
            salt=used_salt,
            length=ARGON2_KEY_LENGTH,
            iterations=iterations,
            lanes=ARGON2_LANES,
            memory_cost=memory_cost,
        )

        raw_key = kdf.derive(master_password.encode("utf-8"))
        return base64.urlsafe_b64encode(raw_key)
    def unlock(self, master_password: str) -> None:
        fernet_class, invalid_token_class, _argon2id_class = self._crypto_dependencies()

        if not VAULT_FILE.exists():
            key = self._derive_key(master_password)
            self.fernet = fernet_class(key)
            self.derived_key = key
            self.entries = []
            self.save()
            return

        encrypted_data, vault_salt = self._read_vault_file()
        used_salt = vault_salt if vault_salt is not None else self._load_or_create_salt()
        key = self._derive_key(master_password, salt=used_salt)
        self.fernet = fernet_class(key)
        self.derived_key = key
        used_legacy_params = False

        try:
            decrypted_data = self.fernet.decrypt(encrypted_data)
        except invalid_token_class:
            legacy_key = self._derive_key(
                master_password,
                salt=used_salt,
                iterations=LEGACY_ARGON2_ITERATIONS,
                memory_cost=LEGACY_ARGON2_MEMORY_COST,
            )
            legacy_fernet = fernet_class(legacy_key)

            try:
                decrypted_data = legacy_fernet.decrypt(encrypted_data)
            except invalid_token_class as exc:
                self.fernet = None
                self.derived_key = None
                self.entries.clear()
                raise ValueError("Neplatné hlavní heslo nebo poškozený trezor.") from exc

            used_legacy_params = True

        self.salt = used_salt
        raw_entries = json.loads(decrypted_data.decode("utf-8"))

        self.entries = []
        changed = False

        for item in raw_entries:
            if "id" not in item or not item["id"]:
                item["id"] = str(uuid.uuid4())
                changed = True

            self.entries.append(PasswordEntry(**item))

        # Migrace starších záznamů bez ID a starých pomalejších KDF parametrů.
        if changed or used_legacy_params:
            self.save()

    def verify_master_password(self, master_password: str) -> bool:
        if self.fernet is None or self.derived_key is None:
            return False

        try:
            used_salt = self.salt if self.salt is not None else self._load_or_create_salt()
            key = self._derive_key(master_password, salt=used_salt)
            return secrets.compare_digest(key, self.derived_key)
        except Exception:
            return False
    def change_master_password(self, old_password: str, new_password: str) -> None:
        if not self.verify_master_password(old_password):
            raise ValueError("Původní hlavní heslo není správné.")

        if self.fernet is None:
            raise RuntimeError("Trezor není odemčený.")

        original_salt = self.salt if self.salt is not None else self._load_or_create_salt()
        original_fernet = self.fernet
        original_derived_key = self.derived_key

        new_salt = os.urandom(16)

        try:
            fernet_class, _invalid_token_class, _argon2id_class = self._crypto_dependencies()
            new_key = self._derive_key(new_password, salt=new_salt)
            new_fernet = fernet_class(new_key)
            encrypted_data = self._encrypt_entries(new_fernet)
            self._write_vault_atomic(encrypted_data, new_salt)
            self.salt = new_salt
            self.fernet = new_fernet
            self.derived_key = new_key

        except Exception:
            # Pokud se něco pokazí, vrátíme původní stav v paměti.
            self.salt = original_salt
            self.fernet = original_fernet
            self.derived_key = original_derived_key
            raise

    def _encrypt_entries(self, fernet: Any) -> bytes:
        raw_entries = [asdict(entry) for entry in self.entries]
        json_data = json.dumps(raw_entries, ensure_ascii=False, indent=2).encode("utf-8")
        return fernet.encrypt(json_data)

    def _write_vault_atomic(self, encrypted_data: bytes, salt: bytes) -> None:
        envelope = {
            "version": VAULT_FILE_FORMAT_VERSION,
            "salt": base64.b64encode(salt).decode("ascii"),
            "payload": base64.b64encode(encrypted_data).decode("ascii"),
        }
        vault_data = json.dumps(envelope, ensure_ascii=False, separators=(",", ":")).encode("utf-8")

        with open(TEMP_VAULT_FILE, "wb") as temp_file:
            temp_file.write(vault_data)
            temp_file.flush()
            os.fsync(temp_file.fileno())

        os.replace(TEMP_VAULT_FILE, VAULT_FILE)

    def save(self) -> None:
        if self.fernet is None:
            raise RuntimeError("Trezor není odemčený.")

        encrypted_data = self._encrypt_entries(self.fernet)
        self._write_vault_atomic(encrypted_data, self._load_or_create_salt())

    def lock(self) -> None:
        self.entries.clear()
        self.fernet = None
        self.derived_key = None

    def add(self, entry: PasswordEntry) -> None:
        self.entries.append(entry)
        self.save()

    def find_index_by_id(self, entry_id: str) -> int | None:
        for index, entry in enumerate(self.entries):
            if entry.id == entry_id:
                return index
        return None

    def find_by_id(self, entry_id: str) -> PasswordEntry | None:
        index = self.find_index_by_id(entry_id)
        if index is None:
            return None
        return self.entries[index]

    def update(self, entry_id: str, entry: PasswordEntry) -> None:
        index = self.find_index_by_id(entry_id)
        if index is None:
            raise ValueError("Záznam už neexistuje.")

        entry.id = entry_id
        self.entries[index] = entry
        self.save()

    def delete(self, entry_id: str) -> None:
        index = self.find_index_by_id(entry_id)
        if index is None:
            raise ValueError("Záznam už neexistuje.")

        del self.entries[index]
        self.save()


class PasswordManagerApp(tb.Window):
    def __init__(self) -> None:
        super().__init__(themename="superhero")

        self.color_scheme_id = self._load_color_scheme_id()
        self.language_id = self._load_language_id()
        self.colors = COLOR_SCHEMES[self.color_scheme_id]
        self.scrollbar_style_configured = False
        self._apply_color_scheme_style()

        self.title(self._t("app_title"))
        self.geometry("1280x780")
        self.minsize(1180, 680)
        self._apply_window_icon()

        self.vault = Vault()
        self.selected_entry_id: str | None = None
        self.password_visible = False
        self.clipboard_clear_job: str | None = None
        self.last_copied_password: str | None = None
        self.auto_lock_job: str | None = None
        self.is_unlocked = False
        self.current_view = "login"
        self.entry_cards: dict[str, tb.Frame] = {}
        self.is_creating_new_entry = False
        self.search_refresh_job: str | None = None
        self.list_render_job: str | None = None
        self.list_render_generation = 0
        self.active_category_filter = "Vše"
        self.category_buttons: dict[str, tb.Button] = {}
        self.list_canvas_width = 0

        self._bind_activity_tracking()
        self._build_login_screen()
        self.after(150, self._apply_window_icon)

    def _apply_window_icon(self) -> None:
        icon_bitmap_path = str(APP_ICON_BITMAP_FILE.resolve())
        icon_png_path = str(APP_ICON_FILE.resolve())

        if APP_ICON_FILE.exists():
            try:
                self.window_icon = tk.PhotoImage(file=icon_png_path)
                self.iconphoto(False, self.window_icon)
            except TclError:
                self.window_icon = None

        try:
            if APP_ICON_BITMAP_FILE.exists():
                self.iconbitmap(icon_bitmap_path)
                self.wm_iconbitmap(icon_bitmap_path)
                self.iconbitmap(default=icon_bitmap_path)
        except TclError:
            pass

    def _hex_to_rgb(self, color: str) -> tuple[int, int, int]:
        color = color.lstrip("#")
        return tuple(int(color[index:index + 2], 16) for index in (0, 2, 4))

    def _rgb_to_hex(self, rgb: tuple[int, int, int]) -> str:
        return "#{:02x}{:02x}{:02x}".format(*rgb)

    def _mix_color(self, color: str, target: str, ratio: float) -> str:
        source_rgb = self._hex_to_rgb(color)
        target_rgb = self._hex_to_rgb(target)
        mixed = tuple(
            round(source + (target_value - source) * ratio)
            for source, target_value in zip(source_rgb, target_rgb)
        )
        return self._rgb_to_hex(mixed)

    def _derived_colors(self) -> dict[str, str]:
        colors = self.colors
        return {
            "app_bg": self._mix_color(colors["bg"], "#000000", 0.22),
            "panel": self._mix_color(colors["bg"], colors["field"], 0.34),
            "panel_high": self._mix_color(colors["field"], "#ffffff", 0.08),
            "panel_low": self._mix_color(colors["bg"], "#000000", 0.08),
            "border_soft": self._mix_color(colors["border"], colors["bg"], 0.62),
            "muted_deep": self._mix_color(colors["muted"], colors["bg"], 0.18),
            "danger": "#ef4444",
        }

    def _load_color_scheme_id(self) -> str:
        try:
            if SETTINGS_FILE.exists():
                settings = json.loads(SETTINGS_FILE.read_text(encoding="utf-8"))
                scheme_id = settings.get("color_scheme", DEFAULT_COLOR_SCHEME)
                if scheme_id in COLOR_SCHEMES:
                    return scheme_id
        except (OSError, json.JSONDecodeError):
            pass

        return DEFAULT_COLOR_SCHEME

    def _load_language_id(self) -> str:
        try:
            if SETTINGS_FILE.exists():
                settings = json.loads(SETTINGS_FILE.read_text(encoding="utf-8"))
                language_id = settings.get("language", DEFAULT_LANGUAGE)
                if language_id in LANGUAGES:
                    return language_id
        except (OSError, json.JSONDecodeError):
            pass

        return DEFAULT_LANGUAGE

    def _save_color_scheme_id(self) -> None:
        APP_DIR.mkdir(parents=True, exist_ok=True)
        SETTINGS_FILE.write_text(
            json.dumps(
                {
                    "color_scheme": self.color_scheme_id,
                    "language": self.language_id,
                },
                ensure_ascii=False,
                indent=2,
            ),
            encoding="utf-8",
        )

    def _t(self, key: str) -> str:
        return TRANSLATIONS.get(self.language_id, TRANSLATIONS[DEFAULT_LANGUAGE]).get(
            key,
            TRANSLATIONS[DEFAULT_LANGUAGE].get(key, key),
        )

    def _category_label(self, category: str) -> str:
        return self._t(CATEGORY_TRANSLATION_KEYS.get(category, category))

    def _apply_color_scheme_style(self) -> None:
        style = self.style
        colors = self.colors
        derived = self._derived_colors()

        self.configure(background=derived["app_bg"])

        style.configure(".", background=derived["app_bg"], foreground=colors["text"])
        style.configure("TFrame", background=derived["app_bg"])
        style.configure("TLabel", background=derived["app_bg"], foreground=colors["text"])
        style.configure("App.TFrame", background=derived["app_bg"])
        style.configure("Panel.TFrame", background=derived["panel"])
        style.configure("Card.TFrame", background=derived["panel"])
        style.configure("CardHigh.TFrame", background=derived["panel_high"])
        style.configure("Sidebar.TFrame", background=derived["panel_low"])
        style.configure("ListPane.TFrame", background=colors["panel"])
        style.configure("DetailPane.TFrame", background=derived["app_bg"])
        style.configure("SelectedCard.TFrame", background=derived["panel_high"])
        style.configure("BottomBar.TFrame", background=derived["panel"])
        style.configure("IconTile.TFrame", background=derived["panel_high"])
        style.configure("AccentIcon.TFrame", background=colors["primary"])
        style.configure("NavPill.TFrame", background=derived["panel_high"])
        style.configure("Title.TLabel", background=derived["app_bg"], foreground=colors["text"], font=("Segoe UI", 18, "bold"))
        style.configure("HeaderTitle.TLabel", background=derived["app_bg"], foreground=colors["accent"], font=("Segoe UI", 15, "bold"))
        style.configure("Muted.TLabel", background=derived["app_bg"], foreground=colors["muted"])
        style.configure("CardTitle.TLabel", background=derived["panel"], foreground=colors["text"], font=("Segoe UI", 10, "bold"))
        style.configure("CardMuted.TLabel", background=derived["panel"], foreground=colors["muted"], font=("Segoe UI", 9))
        style.configure("SelectedCardTitle.TLabel", background=derived["panel_high"], foreground=colors["text"], font=("Segoe UI", 10, "bold"))
        style.configure("SelectedCardMuted.TLabel", background=derived["panel_high"], foreground=colors["muted"], font=("Segoe UI", 9))
        style.configure("SidebarTitle.TLabel", background=derived["panel_low"], foreground=colors["accent"], font=("Segoe UI", 16, "bold"))
        style.configure("SidebarText.TLabel", background=derived["panel_low"], foreground=colors["text"], font=("Segoe UI", 11))
        style.configure("SidebarMuted.TLabel", background=derived["panel_low"], foreground=colors["muted"], font=("Segoe UI", 9))
        style.configure("PanelTitle.TLabel", background=derived["panel"], foreground=colors["text"], font=("Segoe UI", 18, "bold"))
        style.configure("PanelMuted.TLabel", background=derived["panel"], foreground=colors["muted"])
        style.configure("DetailTitle.TLabel", background=derived["app_bg"], foreground=colors["text"], font=("Segoe UI", 22, "bold"))
        style.configure("DetailMuted.TLabel", background=derived["app_bg"], foreground=colors["muted"], font=("Segoe UI", 11))
        style.configure("DetailFieldLabel.TLabel", background=derived["app_bg"], foreground=colors["accent"], font=("Consolas", 10, "bold"))
        style.configure("FieldLabel.TLabel", background=derived["panel"], foreground=colors["accent"], font=("Consolas", 9, "bold"))
        style.configure("IconText.TLabel", background=derived["panel_high"], foreground=colors["accent"], font=("Segoe UI", 15, "bold"))
        style.configure("AccentIconText.TLabel", background=colors["primary"], foreground=colors["bg"], font=("Segoe UI", 22, "bold"))
        style.configure("NavLabel.TLabel", background=derived["panel"], foreground=colors["muted"], font=("Consolas", 9, "bold"))
        style.configure("NavActive.TLabel", background=derived["panel_high"], foreground=colors["accent"], font=("Consolas", 9, "bold"))

        style.configure(
            "TLabelframe",
            background=derived["panel"],
            bordercolor=derived["border_soft"],
            lightcolor=derived["border_soft"],
            darkcolor=derived["border_soft"],
        )
        style.configure(
            "TLabelframe.Label",
            background=derived["panel"],
            foreground=colors["accent"],
        )

        style.configure(
            "TEntry",
            fieldbackground=colors["field"],
            foreground=colors["text"],
            insertcolor=colors["text"],
            bordercolor=derived["border_soft"],
            lightcolor=derived["border_soft"],
            darkcolor=derived["border_soft"],
            padding=(12, 9),
        )
        style.configure(
            "Petroleum.TEntry",
            fieldbackground=colors["field"],
            foreground=colors["text"],
            insertcolor=colors["text"],
            bordercolor=derived["border_soft"],
            lightcolor=derived["border_soft"],
            darkcolor=derived["border_soft"],
            padding=(12, 9),
        )
        style.map(
            "Petroleum.TEntry",
            bordercolor=[("focus", colors["accent"])],
            lightcolor=[("focus", colors["accent"])],
            darkcolor=[("focus", colors["accent"])],
        )

        style.configure(
            "Treeview",
            background=colors["field"],
            fieldbackground=colors["field"],
            foreground=colors["text"],
            bordercolor=colors["border"],
            rowheight=28,
        )
        style.map(
            "Treeview",
            background=[("selected", colors["primary"])],
            foreground=[("selected", colors["text"])],
        )
        style.configure(
            "Treeview.Heading",
            background=colors["primary"],
            foreground=colors["text"],
            font=("Segoe UI", 10, "bold"),
        )
        style.configure(
            "Petroleum.Treeview",
            background=colors["field"],
            fieldbackground=colors["field"],
            foreground=colors["text"],
            bordercolor=colors["border"],
            rowheight=28,
        )
        style.map(
            "Petroleum.Treeview",
            background=[("selected", colors["primary"])],
            foreground=[("selected", colors["text"])],
        )
        style.configure(
            "Petroleum.Treeview.Heading",
            background=colors["primary"],
            foreground=colors["text"],
            font=("Segoe UI", 10, "bold"),
        )

        if not self.scrollbar_style_configured:
            try:
                style.configure(
                    "TScrollbar",
                    background=colors["primary"],
                    troughcolor=derived["app_bg"],
                    bordercolor=derived["app_bg"],
                    arrowcolor=colors["text"],
                )
                self.scrollbar_style_configured = True
            except TclError:
                self.scrollbar_style_configured = True

        style.configure(
            "LoginCard.TFrame",
            background=derived["panel"],
        )
        style.configure(
            "LoginTitle.TLabel",
            background=derived["app_bg"],
            foreground=colors["text"],
            font=("Segoe UI", 24, "bold"),
        )
        style.configure(
            "LoginMuted.TLabel",
            background=derived["app_bg"],
            foreground=colors["muted"],
        )
        style.configure("LoginPanelMuted.TLabel", background=derived["panel"], foreground=colors["muted"])
        style.configure("LoginPanelAccent.TLabel", background=derived["panel"], foreground=colors["accent"], font=("Consolas", 9, "bold"))
        style.configure("LoginPanelIcon.TLabel", background=derived["panel_high"], foreground=colors["accent"], font=("Segoe UI", 22, "bold"))
        style.configure(
            "Login.TEntry",
            fieldbackground=colors["login_field"],
            foreground=colors["text"],
            insertcolor=colors["text"],
            bordercolor=derived["border_soft"],
            lightcolor=derived["border_soft"],
            darkcolor=derived["border_soft"],
            padding=(12, 10),
        )
        style.configure(
            "LoginAccent.TButton",
            background=colors["login_button"],
            foreground=colors["text"],
            bordercolor=colors["login_button"],
            lightcolor=colors["login_button"],
            darkcolor=colors["login_button"],
            font=("Segoe UI", 10, "bold"),
        )
        style.map(
            "LoginAccent.TButton",
            background=[
                ("active", colors["login_button_hover"]),
                ("pressed", colors["login_button_pressed"]),
                ("disabled", colors["disabled"]),
            ],
            foreground=[("disabled", colors["muted"])],
            bordercolor=[("active", colors["login_button_hover"])],
        )
        style.configure(
            "Ghost.TButton",
            background=derived["app_bg"],
            foreground=colors["muted"],
            bordercolor=derived["app_bg"],
            lightcolor=derived["app_bg"],
            darkcolor=derived["app_bg"],
            font=("Segoe UI", 11, "bold"),
        )
        style.map("Ghost.TButton", background=[("active", derived["panel"])], foreground=[("active", colors["accent"])])
        style.configure(
            "Sidebar.TButton",
            background=derived["panel_low"],
            foreground=colors["muted"],
            bordercolor=derived["panel_low"],
            lightcolor=derived["panel_low"],
            darkcolor=derived["panel_low"],
            font=("Segoe UI", 11),
            padding=(14, 9),
        )
        style.map("Sidebar.TButton", background=[("active", derived["panel"])], foreground=[("active", colors["text"])])
        style.configure(
            "SidebarActive.TButton",
            background=derived["panel_high"],
            foreground=colors["text"],
            bordercolor=derived["panel_high"],
            lightcolor=derived["panel_high"],
            darkcolor=derived["panel_high"],
            font=("Segoe UI", 11),
            padding=(14, 9),
        )
        style.configure(
            "Icon.TButton",
            background=derived["panel"],
            foreground=colors["accent"],
            bordercolor=derived["panel"],
            lightcolor=derived["panel"],
            darkcolor=derived["panel"],
            font=("Segoe UI", 12, "bold"),
            padding=(8, 6),
        )
        style.map("Icon.TButton", background=[("active", derived["panel_high"]), ("pressed", derived["panel_high"])])
        style.configure(
            "PrimaryWide.TButton",
            background=colors["primary"],
            foreground=colors["bg"],
            bordercolor=colors["primary"],
            lightcolor=colors["primary"],
            darkcolor=colors["primary"],
            font=("Segoe UI", 10, "bold"),
            padding=(16, 10),
        )
        style.map(
            "PrimaryWide.TButton",
            background=[("active", colors["primary_hover"]), ("pressed", colors["login_button_pressed"]), ("disabled", colors["disabled"])],
            foreground=[("disabled", colors["muted"])],
        )
        style.configure(
            "SecondaryWide.TButton",
            background=derived["panel"],
            foreground=colors["text"],
            bordercolor=derived["border_soft"],
            lightcolor=derived["border_soft"],
            darkcolor=derived["border_soft"],
            font=("Segoe UI", 10, "bold"),
            padding=(16, 10),
        )
        style.configure(
            "DangerOutline.TButton",
            background=derived["app_bg"],
            foreground=derived["danger"],
            bordercolor=derived["danger"],
            lightcolor=derived["danger"],
            darkcolor=derived["danger"],
            font=("Segoe UI", 10, "bold"),
            padding=(16, 10),
        )

        # Barvy tlačítek necháváme z původního ttkbootstrap tématu.
        # Vybraná paleta se aplikuje hlavně na pozadí, panely, pole a tabulku.


    def _bind_activity_tracking(self) -> None:
        activity_events = (
            "<Any-KeyPress>",
            "<Any-Button>",
            "<Motion>",
            "<MouseWheel>",
        )

        for event_name in activity_events:
            self.bind_all(event_name, self._reset_auto_lock_timer, add="+")

    def _reset_auto_lock_timer(self, _event=None) -> None:
        if not self.is_unlocked:
            return

        if self.auto_lock_job is not None:
            self.after_cancel(self.auto_lock_job)

        self.auto_lock_job = self.after(
            AUTO_LOCK_SECONDS * 1000,
            self._auto_lock_due_to_inactivity,
        )

    def _auto_lock_due_to_inactivity(self) -> None:
        if not self.is_unlocked:
            return

        self._lock_application(show_message=True)

    def _lock_application(self, show_message: bool = False) -> None:
        if self.clipboard_clear_job is not None:
            self.after_cancel(self.clipboard_clear_job)
            self.clipboard_clear_job = None

        self._clear_clipboard_if_unchanged()

        self.vault.lock()
        self.selected_entry_id = None
        self.is_unlocked = False

        if self.auto_lock_job is not None:
            self.after_cancel(self.auto_lock_job)
            self.auto_lock_job = None

        self._build_login_screen()

        if show_message:
            Messagebox.show_info(
                "Aplikace byla kvůli nečinnosti uzamčena." if self.language_id == "cs" else "The app was locked due to inactivity.",
                "Automatické uzamčení" if self.language_id == "cs" else "Auto lock",
            )

    def _clear_window(self) -> None:
        if self.search_refresh_job is not None:
            try:
                self.after_cancel(self.search_refresh_job)
            except TclError:
                pass
            self.search_refresh_job = None

        self._cancel_list_render_job()

        for widget in self.winfo_children():
            widget.destroy()

    def _build_login_screen(self) -> None:
        self._clear_window()
        self.current_view = "login"
        derived = self._derived_colors()

        wrapper = tb.Frame(self, padding=40, style="App.TFrame")
        wrapper.pack(expand=True, fill=BOTH)

        content = tb.Frame(wrapper, style="App.TFrame")
        content.place(relx=0.5, rely=0.52, anchor=CENTER, width=450)

        emblem = tb.Frame(content, padding=12, style="IconTile.TFrame")
        emblem.pack(pady=(0, 24))
        tb.Label(emblem, text="LOCK", style="LoginPanelIcon.TLabel").pack()

        tb.Label(
            content,
            text="Trezor",
            style="LoginTitle.TLabel",
        ).pack(pady=(0, 8))

        vault_exists = VAULT_FILE.exists()
        subtitle_text = (
            self._t("unlock_subtitle")
            if vault_exists
            else self._t("create_subtitle")
        )

        tb.Label(
            content,
            text=subtitle_text,
            font=("Segoe UI", 11),
            style="LoginMuted.TLabel",
        ).pack(pady=(0, 40))

        card = tb.Frame(content, padding=32, style="LoginCard.TFrame")
        card.pack(fill=X)

        tb.Label(card, text=self._t("master_password"), style="LoginPanelAccent.TLabel").pack(anchor=W, pady=(0, 10))

        self.master_password_var = tb.StringVar()
        password_entry = tb.Entry(
            card,
            textvariable=self.master_password_var,
            show="•",
            width=36,
            font=("Segoe UI", 12),
            style="Login.TEntry",
        )
        password_entry.pack(fill=X, pady=(0, 14), ipady=2)
        password_entry.focus()

        self.master_password_confirm_var = tb.StringVar()

        if not vault_exists:
            tb.Label(
                card,
                text=self._t("confirm_master_password"),
                font=("Segoe UI", 9),
                style="LoginPanelAccent.TLabel",
            ).pack(anchor=W)

            confirm_entry = tb.Entry(
                card,
                textvariable=self.master_password_confirm_var,
                show="•",
                width=36,
                font=("Segoe UI", 12),
                style="Login.TEntry",
            )
            confirm_entry.pack(fill=X, pady=(8, 14), ipady=2)

        button_text = self._t("unlock_vault") if vault_exists else self._t("create_vault")

        tb.Button(
            card,
            text=button_text,
            style="LoginAccent.TButton",
            command=self._unlock_vault,
        ).pack(fill=X, pady=(10, 24), ipady=8)

        tk.Frame(card, height=1, background=derived["border_soft"]).pack(fill=X, pady=(0, 24))

        hint_text = (
            self._t("login_hint")
            if vault_exists
            else self._t("create_hint")
        )

        tb.Label(
            card,
            text=hint_text,
            font=("Segoe UI", 9),
            wraplength=330,
            justify=LEFT,
            style="LoginPanelMuted.TLabel",
        ).pack(anchor=W)

        footer = tb.Frame(content, style="App.TFrame")
        footer.pack(pady=(46, 0))
        tb.Label(
            footer,
            text="AES-256 šifrování    Verze 20",
            font=("Consolas", 8, "bold"),
            style="Muted.TLabel",
        ).pack()

        self.bind("<Return>", self._handle_login_enter)
    def _handle_login_enter(self, _event=None) -> None:
        # Enter má odemykat pouze na přihlašovací obrazovce.
        # Při prázdném poli neděláme nic, aby se při startu nezobrazovala hláška o délce hesla.
        if not hasattr(self, "master_password_var"):
            return

        if not self.master_password_var.get():
            return

        self._unlock_vault()

    def _unlock_vault(self) -> None:
        password = self.master_password_var.get()
        vault_exists = VAULT_FILE.exists()

        if len(password) < 8:
            Messagebox.show_warning(
                "Hlavní heslo by mělo mít alespoň 8 znaků.",
                "Slabé hlavní heslo",
            )
            return

        if password != password.strip():
            Messagebox.show_warning(
                "Hlavní heslo nesmí začínat ani končit mezerou.",
                "Neplatné hlavní heslo",
            )
            return

        if not vault_exists:
            confirm_password = self.master_password_confirm_var.get()

            if not confirm_password:
                Messagebox.show_warning(
                    "Zadej hlavní heslo ještě jednou pro potvrzení.",
                    "Chybí potvrzení hesla",
                )
                return

            if password != confirm_password:
                Messagebox.show_error(
                    "Zadaná hlavní hesla se neshodují. Trezor nebyl vytvořen.",
                    "Hesla se neshodují",
                )
                return

        try:
            self.vault.unlock(password)
        except ValueError as exc:
            Messagebox.show_error(str(exc), "Nelze odemknout")
            return

        self.is_unlocked = True
        self._build_main_screen()
        self._reset_auto_lock_timer()

    def _build_main_screen(self) -> None:
        self._clear_window()
        self.unbind("<Return>")
        self.current_view = "list"
        self.entry_cards = {}
        derived = self._derived_colors()

        if self.is_creating_new_entry:
            selected_entry = None
        else:
            selected_entry = self.vault.find_by_id(self.selected_entry_id) if self.selected_entry_id else None
            if (
                selected_entry is not None
                and self.active_category_filter != "Vše"
                and selected_entry.category != self.active_category_filter
            ):
                selected_entry = None
                self.selected_entry_id = None

            if selected_entry is None:
                filtered_entries = self._filtered_entries()
                selected_entry = filtered_entries[0] if filtered_entries else None
            if selected_entry is not None:
                self.selected_entry_id = selected_entry.id

        shell = tb.Frame(self, style="App.TFrame")
        shell.pack(fill=BOTH, expand=True)
        shell.rowconfigure(0, weight=1)
        shell.columnconfigure(0, minsize=250)
        shell.columnconfigure(1, minsize=390)
        shell.columnconfigure(2, weight=1)

        sidebar = tb.Frame(shell, padding=(16, 24), style="Sidebar.TFrame")
        sidebar.grid(row=0, column=0, sticky="nsew")
        sidebar.rowconfigure(8, weight=1)

        tb.Label(sidebar, text="Trezor", style="SidebarTitle.TLabel").grid(row=0, column=0, sticky="w", pady=(0, 28))
        tb.Button(sidebar, text=self._t("new_entry"), style="PrimaryWide.TButton", command=self._new_entry).grid(row=1, column=0, sticky="ew", pady=(0, 18), ipady=6)
        tb.Button(sidebar, text=self._t("import_chrome"), style="SecondaryWide.TButton", command=self._import_chrome_csv).grid(row=2, column=0, sticky="ew", pady=(0, 18), ipady=5)
        self._sidebar_nav_item(sidebar, 3, "Trezor", True, self._show_vault_home)

        account = tb.Frame(sidebar, style="Sidebar.TFrame")
        account.grid(row=9, column=0, sticky="ew", pady=(0, 18))
        account.columnconfigure(1, weight=1)
        avatar = tb.Frame(account, padding=(8, 6), style="IconTile.TFrame")
        avatar.grid(row=0, column=0, rowspan=2, padx=(0, 10))
        tb.Label(avatar, text="OK", style="IconText.TLabel").pack()
        tb.Label(account, text=self._t("local_vault"), style="SidebarText.TLabel").grid(row=0, column=1, sticky="w")
        self.sidebar_count_var = tb.StringVar(value=self._vault_count_text())
        tb.Label(account, textvariable=self.sidebar_count_var, style="SidebarMuted.TLabel").grid(row=1, column=1, sticky="w")
        self._sidebar_nav_item(sidebar, 10, self._t("settings"), False, self._open_settings_dialog)
        self._sidebar_nav_item(sidebar, 11, self._t("lock"), False, lambda: self._lock_application(show_message=False))

        list_pane = tb.Frame(shell, padding=(24, 28), style="ListPane.TFrame")
        list_pane.grid(row=0, column=1, sticky="nsew")
        list_pane.rowconfigure(3, weight=1)
        list_pane.columnconfigure(0, weight=1)
        tk.Frame(shell, width=1, background=derived["border_soft"]).grid(row=0, column=1, sticky="nse")

        tb.Label(list_pane, text=self._t("all_records"), style="PanelTitle.TLabel").grid(row=0, column=0, sticky="w", pady=(0, 22))
        self.search_var = tb.StringVar()
        self.search_var.trace_add("write", lambda *_args: self._schedule_refresh_list())
        search_entry = tb.Entry(list_pane, textvariable=self.search_var, font=("Segoe UI", 11), style="Petroleum.TEntry")
        search_entry.grid(row=1, column=0, sticky="ew", pady=(0, 18), ipady=2)

        self.category_filter_frame = tb.Frame(list_pane, style="ListPane.TFrame")
        self.category_filter_frame.grid(row=2, column=0, sticky="w", pady=(0, 22))
        self._render_category_filters()
        list_shell = tb.Frame(list_pane, style="ListPane.TFrame")
        list_shell.grid(row=3, column=0, sticky="nsew")
        list_shell.rowconfigure(0, weight=1)
        list_shell.columnconfigure(0, weight=1)
        list_background = self.colors["panel"]
        self.list_canvas = tk.Canvas(
            list_shell,
            background=list_background,
            bg=list_background,
            highlightthickness=0,
            borderwidth=0,
        )
        self.list_canvas.configure(background=list_background, bg=list_background, yscrollincrement=16)
        scrollbar = tb.Scrollbar(list_shell, orient=VERTICAL, command=self.list_canvas.yview)
        self.list_canvas.configure(yscrollcommand=scrollbar.set)
        self.list_canvas.grid(row=0, column=0, sticky="nsew")
        scrollbar.grid(row=0, column=1, sticky="ns")
        self.list_container = tb.Frame(self.list_canvas, style="ListPane.TFrame")
        self.list_container.columnconfigure(0, weight=1)
        self.list_window = self.list_canvas.create_window((0, 0), window=self.list_container, anchor="nw")
        self.list_container.bind("<Configure>", self._sync_list_scroll_region)
        self.list_canvas.bind("<Configure>", self._sync_list_canvas_width)
        self._bind_list_scrolling(self.list_canvas)
        self._bind_list_scrolling(self.list_container)

        self.detail_container = tb.Frame(shell, padding=(40, 28, 40, 86), style="DetailPane.TFrame")
        self.detail_container.grid(row=0, column=2, sticky="nsew")
        self.detail_container.rowconfigure(2, weight=1)
        self.detail_container.columnconfigure(0, weight=1)
        tk.Frame(shell, width=1, background=derived["border_soft"]).grid(row=0, column=2, sticky="nsw")

        self._refresh_list()
        self._render_detail_panel(selected_entry)

    def _vault_count_text(self) -> str:
        return self._t("record_count").format(count=len(self.vault.entries))

    def _refresh_sidebar_count(self) -> None:
        if hasattr(self, "sidebar_count_var"):
            self.sidebar_count_var.set(self._vault_count_text())

    def _sidebar_nav_item(self, parent: tb.Frame, row: int, label: str, active: bool, command) -> None:
        style_name = "SidebarActive.TButton" if active else "Sidebar.TButton"
        button = tb.Button(parent, text=label, style=style_name, command=command)
        button.grid(row=row, column=0, sticky="ew", pady=(0, 8), ipady=4)

    def _show_vault_home(self) -> None:
        self.is_creating_new_entry = False
        self._build_main_screen()

    def _sync_list_scroll_region(self, _event=None) -> None:
        if hasattr(self, "list_canvas"):
            self.list_canvas.configure(scrollregion=self.list_canvas.bbox("all"))

    def _sync_list_canvas_width(self, event) -> None:
        if hasattr(self, "list_canvas") and hasattr(self, "list_window"):
            width = max(1, event.width)
            if abs(width - self.list_canvas_width) < 2:
                return
            self.list_canvas_width = width
            self.list_canvas.itemconfigure(self.list_window, width=width)

    def _bind_list_scrolling(self, widget) -> None:
        widget.bind("<MouseWheel>", self._on_list_mousewheel, add="+")
        widget.bind("<Button-4>", self._on_list_mousewheel, add="+")
        widget.bind("<Button-5>", self._on_list_mousewheel, add="+")

    def _on_list_mousewheel(self, event) -> str:
        if not hasattr(self, "list_canvas"):
            return "break"

        if getattr(event, "num", None) == 4:
            delta = -1
        elif getattr(event, "num", None) == 5:
            delta = 1
        else:
            delta = -1 if event.delta > 0 else 1

        self.list_canvas.yview_scroll(delta, "units")
        return "break"

    def _schedule_refresh_list(self) -> None:
        self._cancel_list_render_job()

        if self.search_refresh_job is not None:
            try:
                self.after_cancel(self.search_refresh_job)
            except TclError:
                pass

        self.search_refresh_job = self.after(70, self._run_scheduled_refresh_list)

    def _run_scheduled_refresh_list(self) -> None:
        self.search_refresh_job = None
        self._refresh_list()

    def _cancel_list_render_job(self) -> None:
        self.list_render_generation += 1
        if self.list_render_job is not None:
            try:
                self.after_cancel(self.list_render_job)
            except TclError:
                pass
            self.list_render_job = None

    def _render_category_filters(self) -> None:
        if not hasattr(self, "category_filter_frame"):
            return

        for widget in self.category_filter_frame.winfo_children():
            widget.destroy()

        for index, chip in enumerate(("Vše", "Oblíbené", "Práce", "Osobní")):
            style_name = "PrimaryWide.TButton" if chip == self.active_category_filter else "SecondaryWide.TButton"
            tb.Button(
                self.category_filter_frame,
                text=self._category_label(chip),
                style=style_name,
                command=lambda value=chip: self._set_category_filter(value),
            ).grid(row=0, column=index, padx=(0, 8), ipady=0)

    def _set_category_filter(self, category: str) -> None:
        self.active_category_filter = category
        self.is_creating_new_entry = False

        selected_entry = self.vault.find_by_id(self.selected_entry_id) if self.selected_entry_id else None
        if (
            selected_entry is not None
            and category != "Vše"
            and selected_entry.category != category
        ):
            selected_entry = None
            self.selected_entry_id = None

        if selected_entry is None:
            filtered_entries = self._filtered_entries()
            selected_entry = filtered_entries[0] if filtered_entries else None
            self.selected_entry_id = selected_entry.id if selected_entry else None

        self._render_category_filters()
        self._refresh_list()
        if hasattr(self, "detail_container"):
            self._render_detail_panel(selected_entry)

    def _entry_icon(self, entry: PasswordEntry) -> str:
        title = entry.title.strip()
        return title[:2].lower() if len(title) > 1 else (title[:1].upper() if title else "#")

    def _entry_card(self, parent: tb.Frame, entry: PasswordEntry) -> None:
        is_selected = (not self.is_creating_new_entry) and entry.id == self.selected_entry_id
        card_style = "SelectedCard.TFrame" if is_selected else "Card.TFrame"
        title_style = "SelectedCardTitle.TLabel" if is_selected else "CardTitle.TLabel"
        muted_style = "SelectedCardMuted.TLabel" if is_selected else "CardMuted.TLabel"
        row = len(self.entry_cards)

        card = tb.Frame(parent, padding=(14, 12), style=card_style)
        card.grid(row=row, column=0, sticky="ew", pady=(0, 10))
        card.columnconfigure(1, weight=1)
        self.entry_cards[entry.id] = card
        self._bind_list_scrolling(card)

        icon = tb.Frame(card, padding=(10, 8), style="IconTile.TFrame")
        icon.grid(row=0, column=0, rowspan=2, sticky="w", padx=(0, 14))
        tb.Label(icon, text=self._entry_icon(entry), style="IconText.TLabel").pack()

        tb.Label(card, text=entry.title or self._t("untitled"), style=title_style).grid(row=0, column=1, sticky="w")
        tb.Label(card, text=entry.username or entry.url or self._t("no_user"), style=muted_style).grid(row=1, column=1, sticky="w", pady=(3, 0))

        card.bind("<Button-1>", lambda _event, entry_id=entry.id: self._select_entry(entry_id))
        for child in card.winfo_children():
            self._bind_list_scrolling(child)
            child.bind("<Button-1>", lambda _event, entry_id=entry.id: self._select_entry(entry_id))
            for grandchild in child.winfo_children():
                self._bind_list_scrolling(grandchild)
                grandchild.bind("<Button-1>", lambda _event, entry_id=entry.id: self._select_entry(entry_id))

    def _select_entry(self, entry_id: str) -> None:
        entry = self.vault.find_by_id(entry_id)
        if entry is None:
            Messagebox.show_warning(self._t("missing_record"), self._t("record_not_found"))
            self._refresh_list()
            return

        previous_entry_id = self.selected_entry_id
        self.selected_entry_id = entry_id
        self.is_creating_new_entry = False

        if hasattr(self, "detail_container") and hasattr(self, "list_container"):
            self._render_detail_panel(entry)
            self._update_list_selection(previous_entry_id, entry_id)
        else:
            self._build_main_screen()

    def _update_list_selection(self, previous_entry_id: str | None, current_entry_id: str) -> None:
        for entry_id in (previous_entry_id, current_entry_id):
            if not entry_id or entry_id not in self.entry_cards:
                continue

            is_selected = entry_id == current_entry_id
            card = self.entry_cards[entry_id]
            card.configure(style="SelectedCard.TFrame" if is_selected else "Card.TFrame")

            for child in card.winfo_children():
                self._update_card_child_selection(child, is_selected)

    def _update_card_child_selection(self, widget, is_selected: bool) -> None:
        try:
            current_style = widget.cget("style")
        except TclError:
            current_style = ""

        if current_style in ("CardTitle.TLabel", "SelectedCardTitle.TLabel"):
            widget.configure(style="SelectedCardTitle.TLabel" if is_selected else "CardTitle.TLabel")
        elif current_style in ("CardMuted.TLabel", "SelectedCardMuted.TLabel"):
            widget.configure(style="SelectedCardMuted.TLabel" if is_selected else "CardMuted.TLabel")

        for child in widget.winfo_children():
            self._update_card_child_selection(child, is_selected)

    def _open_entry_detail(self, entry_id: str) -> None:
        self._select_entry(entry_id)

    def _build_detail_screen(self, entry_id: str | None = None) -> None:
        previous_entry_id = self.selected_entry_id
        self.selected_entry_id = entry_id
        self.is_creating_new_entry = entry_id is None

        if hasattr(self, "detail_container") and hasattr(self, "list_container"):
            entry = self.vault.find_by_id(entry_id) if entry_id else None
            self._render_detail_panel(entry)
            if previous_entry_id:
                self._update_list_selection(previous_entry_id, "")
        else:
            self._build_main_screen()

    def _render_detail_panel(self, entry: PasswordEntry | None) -> None:
        for widget in self.detail_container.winfo_children():
            widget.destroy()

        derived = self._derived_colors()
        is_new = self.is_creating_new_entry or entry is None
        self.password_visible = False
        self.selected_entry_id = None if is_new else entry.id

        header = tb.Frame(self.detail_container, style="DetailPane.TFrame")
        header.grid(row=1, column=0, sticky="ew", pady=(10, 34))
        header.columnconfigure(1, weight=1)
        icon = tb.Frame(header, padding=(18, 14), style="IconTile.TFrame")
        icon.grid(row=0, column=0, rowspan=2, padx=(0, 22))
        tb.Label(icon, text=("new" if is_new else self._entry_icon(entry)), style="IconText.TLabel").pack()
        heading = self._t("new_record") if is_new else entry.title
        tb.Label(header, text=heading, style="DetailTitle.TLabel").grid(row=0, column=1, sticky="sw")
        tb.Label(header, text=self._t("created_now") if is_new else self._t("created_old"), style="DetailMuted.TLabel").grid(row=1, column=1, sticky="nw")

        form = tb.Frame(self.detail_container, style="DetailPane.TFrame")
        form.grid(row=2, column=0, sticky="nsew")
        form.columnconfigure(0, weight=1)
        form.rowconfigure(13, weight=1)

        self.title_var = tb.StringVar(value="" if is_new else entry.title)
        self.username_var = tb.StringVar(value="" if is_new else entry.username)
        self.password_var = tb.StringVar(value="" if is_new else entry.password)
        self.url_var = tb.StringVar(value="" if is_new else entry.url)
        self.category_var = tb.StringVar(value="Osobní" if is_new else entry.category)
        self.password_var.trace_add("write", lambda *_args: self._update_password_strength())

        self._detail_entry(form, 0, self._t("name"), self.title_var)
        self._detail_entry(form, 2, self._t("user_email"), self.username_var)
        self._detail_category(form, 4)

        tb.Label(form, text=self._t("password"), style="DetailFieldLabel.TLabel").grid(row=6, column=0, sticky="w", pady=(18, 8))
        password_frame = tb.Frame(form, style="DetailPane.TFrame")
        password_frame.grid(row=7, column=0, sticky="ew")
        password_frame.columnconfigure(0, weight=1)
        self.password_entry = tb.Entry(password_frame, textvariable=self.password_var, show="*", font=("Segoe UI", 11), style="Petroleum.TEntry")
        self.password_entry.grid(row=0, column=0, sticky="ew", ipady=3)
        tb.Button(password_frame, text=self._t("show"), style="Icon.TButton", command=self._toggle_password_visibility, width=7).grid(row=0, column=1, padx=(8, 0))
        tb.Button(password_frame, text=self._t("copy_short"), style="Icon.TButton", command=self._copy_password, width=5).grid(row=0, column=2, padx=(8, 0))
        tb.Button(password_frame, text=self._t("generate"), style="SecondaryWide.TButton", command=self._generate_password).grid(row=0, column=3, padx=(8, 0))

        meter = tb.Frame(form, style="DetailPane.TFrame")
        meter.grid(row=8, column=0, sticky="ew", pady=(8, 0))
        meter.columnconfigure((0, 1, 2, 3), weight=1)
        self.password_strength_segments = []
        for index in range(4):
            segment = tk.Frame(meter, height=4, background=derived["border_soft"])
            segment.grid(row=0, column=index, sticky="ew", padx=(0 if index == 0 else 3, 0))
            self.password_strength_segments.append(segment)
        self.password_strength_text = tb.StringVar(value="")
        tb.Label(form, textvariable=self.password_strength_text, style="DetailFieldLabel.TLabel").grid(row=9, column=0, sticky="w", pady=(8, 18))
        self._update_password_strength()
        self._detail_entry(form, 10, "URL", self.url_var)
        tb.Label(form, text=self._t("note"), style="DetailFieldLabel.TLabel").grid(row=12, column=0, sticky="w", pady=(18, 8))
        self.note_text = tb.Text(
            form,
            height=6,
            font=("Segoe UI", 11),
            background=self.colors["field"],
            foreground=self.colors["text"],
            insertbackground=self.colors["text"],
            selectbackground=self.colors["primary"],
            selectforeground=self.colors["text"],
            relief="flat",
            borderwidth=0,
            highlightthickness=1,
            highlightbackground=derived["border_soft"],
            highlightcolor=self.colors["accent"],
        )
        self.note_text.grid(row=13, column=0, sticky="nsew")
        self.note_text.insert("1.0", "" if is_new else entry.note)

        footer = tb.Frame(self.detail_container, padding=(0, 20, 0, 0), style="DetailPane.TFrame")
        footer.grid(row=3, column=0, sticky="ew")
        footer.columnconfigure(0, weight=1)
        footer.columnconfigure(1, minsize=130)
        footer.columnconfigure(2, minsize=190)
        self.delete_button = None
        if not is_new:
            self.delete_button = tb.Button(
                footer,
                text=self._t("delete"),
                style="DangerOutline.TButton",
                command=self._delete_selected_entry,
            )
            self.delete_button.grid(row=0, column=0, sticky="w", ipady=4)
        tb.Button(footer, text=self._t("cancel"), style="Ghost.TButton", command=self._cancel_detail_edit).grid(row=0, column=1, sticky="e", padx=(0, 18), ipady=4)
        save_command = self._add_entry_from_form if is_new else self._save_selected_entry
        save_text = self._t("save") if is_new else self._t("save_changes")
        self.save_button = tb.Button(footer, text=save_text, style="PrimaryWide.TButton", command=save_command)
        self.save_button.grid(row=0, column=2, sticky="ew", ipady=5)
        self.add_button = self.save_button
        self._apply_note_text_colors()

    def _detail_entry(self, parent: tb.Frame, row: int, label: str, variable: tb.StringVar) -> None:
        tb.Label(parent, text=label, style="DetailFieldLabel.TLabel").grid(row=row, column=0, sticky="w", pady=(0, 8))
        tb.Entry(parent, textvariable=variable, font=("Segoe UI", 11), style="Petroleum.TEntry").grid(row=row + 1, column=0, sticky="ew", pady=(0, 18), ipady=3)

    def _detail_category(self, parent: tb.Frame, row: int) -> None:
        tb.Label(parent, text=self._t("tag"), style="DetailFieldLabel.TLabel").grid(row=row, column=0, sticky="w", pady=(0, 8))
        category_row = tb.Frame(parent, style="DetailPane.TFrame")
        category_row.grid(row=row + 1, column=0, sticky="ew", pady=(0, 18))
        category_row.columnconfigure((0, 1, 2), weight=1)

        self.category_buttons = {}
        for index, category in enumerate(("Oblíbené", "Práce", "Osobní")):
            button = tb.Button(
                category_row,
                text=self._category_label(category),
                style="PrimaryWide.TButton" if self.category_var.get() == category else "SecondaryWide.TButton",
                command=lambda value=category: self._set_detail_category(value),
            )
            button.grid(row=0, column=index, sticky="ew", padx=(0 if index == 0 else 8, 0), ipady=2)
            self.category_buttons[category] = button

    def _set_detail_category(self, category: str) -> None:
        self.category_var.set(category)
        for name, button in self.category_buttons.items():
            button.configure(style="PrimaryWide.TButton" if name == category else "SecondaryWide.TButton")

    def _password_strength_score(self, password: str) -> int:
        if not password:
            return 0

        score = 0
        if len(password) >= 8:
            score += 1
        if len(password) >= 12:
            score += 1
        if len(password) >= 16:
            score += 1

        character_groups = (
            any(char.islower() for char in password),
            any(char.isupper() for char in password),
            any(char.isdigit() for char in password),
            any(char in string.punctuation for char in password),
        )
        score += max(0, sum(character_groups) - 1)

        if len(set(password)) < max(3, len(password) // 3):
            score -= 1

        return max(0, min(4, score))

    def _update_password_strength(self) -> None:
        if not hasattr(self, "password_strength_segments") or not hasattr(self, "password_strength_text"):
            return

        password = self.password_var.get() if hasattr(self, "password_var") else ""
        score = self._password_strength_score(password)
        labels = {
            0: self._t("strength_0"),
            1: self._t("strength_1"),
            2: self._t("strength_2"),
            3: self._t("strength_3"),
            4: self._t("strength_4"),
        }

        derived = self._derived_colors()
        for index, segment in enumerate(self.password_strength_segments):
            segment.configure(
                background=self.colors["accent"] if index < score else derived["border_soft"]
            )

        self.password_strength_text.set(labels[score])

    def _cancel_detail_edit(self) -> None:
        self.is_creating_new_entry = False
        if not self.selected_entry_id and self.vault.entries:
            self.selected_entry_id = self.vault.entries[0].id
        self._build_main_screen()

    def _open_password_generator(self) -> None:
        self._build_detail_screen(None)
        self._generate_password()
    def _apply_note_text_colors(self) -> None:
        if not hasattr(self, "note_text"):
            return

        colors = self.colors
        self.note_text.configure(
            background=colors["field"],
            bg=colors["field"],
            foreground=colors["text"],
            fg=colors["text"],
            insertbackground=colors["text"],
            selectbackground=colors["primary"],
            inactiveselectbackground=colors["primary"],
            selectforeground=colors["text"],
            highlightbackground=colors["border"],
            highlightcolor=colors["accent"],
        )
        self.note_text["background"] = colors["field"]
        self.note_text["foreground"] = colors["text"]
        self.note_text.update_idletasks()

    def _confirm_dialog(self, message: str, title: str) -> bool:
        dialog = tb.Toplevel(self)
        dialog.title(title)
        dialog.geometry("430x210")
        dialog.minsize(430, 210)
        dialog.resizable(False, False)
        dialog.transient(self)
        dialog.grab_set()

        confirmed = tk.BooleanVar(value=False)

        wrapper = tb.Frame(dialog, padding=24, style="DetailPane.TFrame")
        wrapper.pack(fill=BOTH, expand=True)
        wrapper.columnconfigure(0, weight=1)

        tb.Label(
            wrapper,
            text=title,
            style="DetailTitle.TLabel",
            font=("Segoe UI", 15, "bold"),
        ).grid(row=0, column=0, sticky="w", pady=(0, 12))

        tb.Label(
            wrapper,
            text=message,
            style="DetailMuted.TLabel",
            wraplength=370,
            justify=LEFT,
        ).grid(row=1, column=0, sticky="ew")

        button_row = tb.Frame(wrapper, style="DetailPane.TFrame")
        button_row.grid(row=2, column=0, sticky="ew", pady=(24, 0))
        button_row.columnconfigure(0, weight=1)
        button_row.columnconfigure(1, minsize=110)
        button_row.columnconfigure(2, minsize=120)

        def close_with(value: bool) -> None:
            confirmed.set(value)
            dialog.destroy()

        tb.Button(
            button_row,
            text=self._t("no"),
            style="Ghost.TButton",
            command=lambda: close_with(False),
            width=10,
        ).grid(row=0, column=1, sticky="e", padx=(0, 12), ipady=3)

        tb.Button(
            button_row,
            text=self._t("yes"),
            style="PrimaryWide.TButton",
            command=lambda: close_with(True),
            width=10,
        ).grid(row=0, column=2, sticky="e", ipady=3)

        dialog.protocol("WM_DELETE_WINDOW", lambda: close_with(False))
        dialog.bind("<Escape>", lambda _event: close_with(False))
        dialog.bind("<Return>", lambda _event: close_with(True))
        dialog.wait_window()
        return confirmed.get()

    def _open_settings_dialog(self) -> None:
        dialog = tb.Toplevel(self)
        dialog.title(self._t("settings"))
        dialog.geometry("440x410")
        dialog.minsize(440, 410)
        dialog.resizable(False, False)
        dialog.transient(self)
        dialog.grab_set()

        wrapper = tb.Frame(dialog, padding=24)
        wrapper.pack(fill=BOTH, expand=True)
        wrapper.columnconfigure(0, weight=1)

        tb.Label(
            wrapper,
            text=self._t("settings"),
            font=("Segoe UI", 16, "bold"),
        ).grid(row=0, column=0, sticky="w", pady=(0, 14))

        tb.Label(wrapper, text=self._t("color_scheme")).grid(row=1, column=0, sticky="w", pady=(0, 4))

        scheme_names = [scheme["name"] for scheme in COLOR_SCHEMES.values()]
        scheme_by_name = {scheme["name"]: scheme_id for scheme_id, scheme in COLOR_SCHEMES.items()}
        selected_scheme_name = tb.StringVar(value=COLOR_SCHEMES[self.color_scheme_id]["name"])

        scheme_select = tb.Combobox(
            wrapper,
            textvariable=selected_scheme_name,
            values=scheme_names,
            state="readonly",
            font=("Segoe UI", 10),
        )
        scheme_select.grid(row=2, column=0, sticky="ew", pady=(0, 14), ipady=2)

        tb.Label(wrapper, text=self._t("language")).grid(row=3, column=0, sticky="w", pady=(0, 4))
        language_by_name = {name: language_id for language_id, name in LANGUAGES.items()}
        selected_language_name = tb.StringVar(value=LANGUAGES.get(self.language_id, LANGUAGES[DEFAULT_LANGUAGE]))
        language_select = tb.Combobox(
            wrapper,
            textvariable=selected_language_name,
            values=list(LANGUAGES.values()),
            state="readonly",
            font=("Segoe UI", 10),
        )
        language_select.grid(row=4, column=0, sticky="ew", pady=(0, 14), ipady=2)

        preview = tb.Frame(wrapper)
        preview.grid(row=5, column=0, sticky="ew", pady=(0, 16))

        def update_preview(_event=None) -> None:
            scheme_id = scheme_by_name.get(selected_scheme_name.get(), DEFAULT_COLOR_SCHEME)
            colors = COLOR_SCHEMES[scheme_id]

            for widget in preview.winfo_children():
                widget.destroy()

            for index, color_key in enumerate(("bg", "field", "primary", "accent")):
                swatch = tb.Label(preview, text="      ")
                swatch.configure(background=colors[color_key])
                swatch.grid(row=0, column=index, padx=(0, 8), ipady=8)

        scheme_select.bind("<<ComboboxSelected>>", update_preview)
        update_preview()

        button_row = tb.Frame(wrapper)
        button_row.grid(row=6, column=0, sticky="ew", pady=(10, 0))
        button_row.columnconfigure(0, weight=1)
        button_row.columnconfigure(1, weight=1)

        tb.Button(
            button_row,
            text=self._t("cancel"),
            bootstyle="secondary",
            command=dialog.destroy,
            width=12,
        ).grid(row=0, column=0, sticky="w", ipady=3)

        tb.Button(
            button_row,
            text=self._t("save"),
            bootstyle="success",
            command=lambda: self._save_settings_dialog(
                dialog,
                scheme_by_name.get(selected_scheme_name.get(), DEFAULT_COLOR_SCHEME),
                language_by_name.get(selected_language_name.get(), DEFAULT_LANGUAGE),
            ),
            width=12,
        ).grid(row=0, column=1, sticky="e", ipady=3)

        scheme_select.focus_set()

    def _save_settings_dialog(self, dialog: tb.Toplevel, scheme_id: str, language_id: str) -> None:
        if scheme_id not in COLOR_SCHEMES:
            scheme_id = DEFAULT_COLOR_SCHEME
        if language_id not in LANGUAGES:
            language_id = DEFAULT_LANGUAGE

        self.color_scheme_id = scheme_id
        self.language_id = language_id
        self.colors = COLOR_SCHEMES[self.color_scheme_id]
        self._save_color_scheme_id()
        self.title(self._t("app_title"))
        try:
            self._apply_color_scheme_style()
        except TclError:
            pass
        dialog.destroy()
        if self.current_view == "detail":
            self._build_detail_screen(self.selected_entry_id)
        elif self.current_view == "list":
            self._build_main_screen()
        else:
            self._build_login_screen()
        Messagebox.show_info(self._t("settings_saved"), self._t("done"))

    def _open_change_master_password_form(self) -> None:
        dialog = tb.Toplevel(self)
        dialog.title(self._t("change_master_password"))
        dialog.geometry("520x390")
        dialog.minsize(520, 390)
        dialog.resizable(False, False)
        dialog.transient(self)
        dialog.grab_set()

        wrapper = tb.Frame(dialog, padding=24)
        wrapper.pack(fill=BOTH, expand=True)
        wrapper.columnconfigure(0, weight=1)

        tb.Label(
            wrapper,
            text=self._t("change_master_password"),
            font=("Segoe UI", 16, "bold"),
        ).grid(row=0, column=0, sticky="w", pady=(0, 18))

        old_password_var = tb.StringVar()
        new_password_var = tb.StringVar()
        new_password_confirm_var = tb.StringVar()

        old_entry = self._dialog_password_entry(wrapper, 1, self._t("old_master_password"), old_password_var)
        self._dialog_password_entry(wrapper, 3, self._t("new_master_password"), new_password_var)
        self._dialog_password_entry(wrapper, 5, self._t("new_master_password_again"), new_password_confirm_var)

        button_row = tb.Frame(wrapper)
        button_row.grid(row=7, column=0, sticky="ew", pady=(24, 0))
        button_row.columnconfigure(0, weight=1)
        button_row.columnconfigure(1, weight=1)

        cancel_button = tb.Button(
            button_row,
            text=self._t("cancel"),
            bootstyle="secondary",
            command=dialog.destroy,
            width=18,
        )
        cancel_button.grid(row=0, column=0, sticky="w", ipady=4)

        change_button = tb.Button(
            button_row,
            text=self._t("change_password"),
            bootstyle="warning",
            width=18,
            command=lambda: self._change_master_password(
                dialog,
                old_password_var.get(),
                new_password_var.get(),
                new_password_confirm_var.get(),
            ),
        )
        change_button.grid(row=0, column=1, sticky="e", ipady=4)

        old_entry.focus_set()

    def _dialog_password_entry(
        self,
        parent: tb.Frame,
        row: int,
        label: str,
        variable: tb.StringVar,
    ) -> tb.Entry:
        tb.Label(parent, text=label).grid(row=row, column=0, sticky="w", pady=(0, 4))

        entry = tb.Entry(
            parent,
            textvariable=variable,
            show="•",
            font=("Segoe UI", 10),
        )
        entry.grid(row=row + 1, column=0, sticky="ew", pady=(0, 12), ipady=3)

        return entry

    def _change_master_password(
        self,
        dialog: tb.Toplevel,
        old_password: str,
        new_password: str,
        new_password_confirm: str,
    ) -> None:
        if not old_password:
            Messagebox.show_warning(self._t("enter_old_password"), self._t("missing_password_title"))
            return

        if len(new_password) < 8:
            Messagebox.show_warning(
                "Nové hlavní heslo by mělo mít alespoň 8 znaků.",
                "Slabé hlavní heslo",
            )
            return

        if new_password != new_password.strip():
            Messagebox.show_warning(
                "Nové hlavní heslo nesmí začínat ani končit mezerou.",
                "Neplatné hlavní heslo",
            )
            return

        if new_password != new_password_confirm:
            Messagebox.show_error(
                "Nové hlavní heslo a jeho potvrzení se neshodují.",
                "Hesla se neshodují",
            )
            return

        if old_password == new_password:
            Messagebox.show_warning(
                "Nové hlavní heslo musí být jiné než původní.",
                "Stejné heslo",
            )
            return

        try:
            self.vault.change_master_password(old_password, new_password)
        except Exception as exc:
            Messagebox.show_error(str(exc), "Změna hesla selhala")
            return

        dialog.destroy()
        Messagebox.show_info(
            "Hlavní heslo bylo změněno a trezor byl přešifrován.",
            "Hotovo",
        )

    def _form_entry(self, parent: tb.Frame, row: int, label: str, variable: tb.StringVar) -> None:
        tb.Label(parent, text=label, style="FieldLabel.TLabel").grid(row=row, column=0, sticky="w", pady=(0, 8))
        tb.Entry(
            parent,
            textvariable=variable,
            font=("Segoe UI", 10),
            style="Petroleum.TEntry",
        ).grid(row=row + 1, column=0, sticky="ew", pady=(0, 14), ipady=2)

    def _filtered_entries(self) -> list[PasswordEntry]:
        search = self.search_var.get().lower().strip() if hasattr(self, "search_var") else ""
        entries = []
        for entry in self.vault.entries:
            if self.active_category_filter != "Vše" and entry.category != self.active_category_filter:
                continue

            haystack = f"{entry.title} {entry.username} {entry.url} {entry.note}".lower()
            if search and search not in haystack:
                continue

            entries.append(entry)

        return entries

    def _refresh_list(self) -> None:
        if not hasattr(self, "list_container"):
            return

        self._cancel_list_render_job()
        self.entry_cards = {}
        current_yview = self.list_canvas.yview() if hasattr(self, "list_canvas") else None

        if hasattr(self, "list_canvas"):
            list_background = self.colors["panel"]
            self.list_canvas.configure(background=list_background, bg=list_background)

        for widget in self.list_container.winfo_children():
            widget.destroy()

        visible_entries = self._filtered_entries()

        if not visible_entries:
            empty = tb.Frame(self.list_container, padding=28, style="Card.TFrame")
            empty.grid(row=0, column=0, sticky="ew", pady=(0, 12))
            tb.Label(empty, text=self._t("no_records"), style="CardTitle.TLabel").pack(anchor=W)
            tb.Label(empty, text=self._t("add_first_record"), style="CardMuted.TLabel").pack(anchor=W, pady=(4, 0))
            if hasattr(self, "list_canvas") and hasattr(self, "list_window"):
                width = max(1, self.list_canvas.winfo_width())
                self.list_canvas_width = width
                self.list_canvas.itemconfigure(self.list_window, width=width)
            self._sync_list_scroll_region()
            if current_yview and hasattr(self, "list_canvas"):
                self.list_canvas.yview_moveto(current_yview[0])
        else:
            generation = self.list_render_generation
            self._render_list_batch(visible_entries, 0, generation, current_yview)

    def _render_list_batch(
        self,
        entries: list[PasswordEntry],
        start_index: int,
        generation: int,
        yview_to_restore: tuple[float, float] | None = None,
    ) -> None:
        if generation != self.list_render_generation or not hasattr(self, "list_container"):
            return

        batch_size = 36
        end_index = min(start_index + batch_size, len(entries))
        for entry in entries[start_index:end_index]:
            self._entry_card(self.list_container, entry)

        if hasattr(self, "list_canvas") and hasattr(self, "list_window"):
            width = max(1, self.list_canvas.winfo_width())
            self.list_canvas_width = width
            self.list_canvas.itemconfigure(self.list_window, width=width)

        self._sync_list_scroll_region()
        if yview_to_restore and start_index == 0 and hasattr(self, "list_canvas"):
            self.list_canvas.yview_moveto(yview_to_restore[0])

        if end_index < len(entries):
            self.list_render_job = self.after(
                1,
                lambda: self._render_list_batch(entries, end_index, generation),
            )
        else:
            self.list_render_job = None

    def _on_tree_select(self, _event=None) -> None:
        return

    def _copy_entry_password(self, entry_id: str) -> None:
        entry = self.vault.find_by_id(entry_id)
        if entry is None:
            Messagebox.show_warning(self._t("missing_record"), self._t("record_not_found"))
            self._refresh_list()
            return

        pyperclip.copy(entry.password)
        self.last_copied_password = entry.password

        if self.clipboard_clear_job is not None:
            self.after_cancel(self.clipboard_clear_job)

        self.clipboard_clear_job = self.after(
            CLIPBOARD_CLEAR_SECONDS * 1000,
            self._clear_clipboard_if_unchanged,
        )

        Messagebox.show_info(
            f"Heslo bylo zkopírováno do schránky. Schránka se automaticky vymaže za {CLIPBOARD_CLEAR_SECONDS} sekund.",
            "Zkopírováno",
        )
    def _entry_from_form(self) -> PasswordEntry | None:
        title = self.title_var.get().strip()
        username = self.username_var.get().strip()
        password = self.password_var.get()
        url = self.url_var.get().strip()
        note = self.note_text.get("1.0", END).strip()
        category = self.category_var.get().strip() if hasattr(self, "category_var") else "Osobní"

        if not title:
            Messagebox.show_warning(self._t("missing_title"), self._t("missing_title_title"))
            return None

        if not password:
            Messagebox.show_warning(self._t("missing_entry_password"), self._t("missing_password_title"))
            return None

        return PasswordEntry(
            title=title,
            username=username,
            password=password,
            url=url,
            note=note,
            category=category if category in ("Oblíbené", "Práce", "Osobní") else "Osobní",
        )

    def _add_entry_from_form(self) -> None:
        entry = self._entry_from_form()
        if entry is None:
            return

        self.vault.add(entry)
        self.selected_entry_id = entry.id
        self.is_creating_new_entry = False
        Messagebox.show_info(self._t("entry_saved"), self._t("done"))
        self._build_main_screen()
    def _import_chrome_csv(self) -> None:
        file_path = filedialog.askopenfilename(
            title="Vyber CSV export z Chrome",
            filetypes=(
                ("CSV soubory", "*.csv"),
                ("Všechny soubory", "*.*"),
            ),
        )

        if not file_path:
            return

        try:
            imported_entries, skipped_rows, duplicate_rows = self._read_chrome_csv(Path(file_path))
        except Exception as exc:
            Messagebox.show_error(
                f"Soubor se nepodařilo načíst jako CSV export z Chrome.\n\n{exc}",
                "Import selhal",
            )
            return

        if not imported_entries:
            Messagebox.show_warning(
                "V souboru nebyly nalezeny žádné nové záznamy s heslem.",
                "Nic k importu",
            )
            return

        confirmed = self._confirm_dialog(
            self._t("import_confirm_message").format(
                count=len(imported_entries),
                skipped=skipped_rows,
                duplicates=duplicate_rows,
            ),
            self._t("confirm_import"),
        )

        if not confirmed:
            return

        self.vault.entries.extend(imported_entries)
        self.vault.save()
        self.selected_entry_id = imported_entries[-1].id
        self.is_creating_new_entry = False
        self.active_category_filter = "Vše"
        self._build_main_screen()
        Messagebox.show_info(
            f"Importováno {len(imported_entries)} záznamů z Chrome.",
            "Import dokončen",
        )

    def _read_chrome_csv(self, file_path: Path) -> tuple[list[PasswordEntry], int, int]:
        rows = self._read_csv_rows(file_path)
        imported_entries: list[PasswordEntry] = []
        skipped_rows = 0
        duplicate_rows = 0

        existing_keys = {
            self._entry_duplicate_key(entry)
            for entry in self.vault.entries
        }

        for row in rows:
            normalized_row = {
                str(key).strip().lower(): (value or "").strip()
                for key, value in row.items()
                if key is not None
            }

            title = self._csv_value(normalized_row, "name", "title", "název")
            url = self._csv_value(normalized_row, "url", "origin_url", "origin url")
            username = self._csv_value(
                normalized_row,
                "username",
                "username_value",
                "user",
                "login",
            )
            password = self._csv_value(normalized_row, "password", "password_value", "heslo")

            if not url or not password:
                skipped_rows += 1
                continue

            entry = PasswordEntry(
                title=title or url,
                username=username,
                password=password,
                url=url,
                note="Importováno z Chrome.",
                category="Osobní",
            )

            duplicate_key = self._entry_duplicate_key(entry)
            if duplicate_key in existing_keys:
                duplicate_rows += 1
                continue

            existing_keys.add(duplicate_key)
            imported_entries.append(entry)

        return imported_entries, skipped_rows, duplicate_rows

    def _read_csv_rows(self, file_path: Path) -> list[dict[str, str]]:
        last_error: Exception | None = None

        for encoding in ("utf-8-sig", "utf-16", "cp1250"):
            try:
                with open(file_path, "r", encoding=encoding, newline="") as csv_file:
                    return list(csv.DictReader(csv_file))
            except UnicodeError as exc:
                last_error = exc

        if last_error is not None:
            raise last_error

        return []

    def _csv_value(self, row: dict[str, str], *names: str) -> str:
        for name in names:
            value = row.get(name)
            if value:
                return value
        return ""

    def _entry_duplicate_key(self, entry: PasswordEntry) -> tuple[str, str, str, str]:
        return (
            entry.title.strip().lower(),
            entry.url.strip().lower(),
            entry.username.strip().lower(),
            entry.password,
        )

    def _save_selected_entry(self) -> None:
        if self.selected_entry_id is None:
            Messagebox.show_warning(self._t("select_record_first"), self._t("no_record_selected"))
            return

        entry = self._entry_from_form()
        if entry is None:
            return

        try:
            self.vault.update(self.selected_entry_id, entry)
        except ValueError as exc:
            Messagebox.show_error(str(exc), "Nelze uložit")
            self._build_main_screen()
            return

        self.is_creating_new_entry = False
        Messagebox.show_info(self._t("changes_saved"), self._t("done"))
        self._build_main_screen()

    def _delete_selected_entry(self) -> None:
        if self.selected_entry_id is None:
            Messagebox.show_warning(self._t("select_record_first"), self._t("no_record_selected"))
            return

        confirmed = self._confirm_dialog(
            self._t("delete_confirm_message"),
            self._t("confirm_delete"),
        )

        if not confirmed:
            return

        try:
            self.vault.delete(self.selected_entry_id)
        except ValueError as exc:
            Messagebox.show_error(str(exc), "Nelze smazat")
            self._refresh_list()
            return

        visible_entries = self._filtered_entries()
        selected_entry = visible_entries[0] if visible_entries else None
        self.selected_entry_id = selected_entry.id if selected_entry else None
        self.is_creating_new_entry = False
        self._refresh_sidebar_count()
        self._refresh_list()
        if hasattr(self, "detail_container"):
            self._render_detail_panel(selected_entry)
        Messagebox.show_info(self._t("entry_deleted"), self._t("done"))

    def _new_entry(self) -> None:
        self.selected_entry_id = None
        self.is_creating_new_entry = True
        self._build_detail_screen(None)

    def _clear_form(self) -> None:
        if hasattr(self, "title_var"):
            self.title_var.set("")
        if hasattr(self, "username_var"):
            self.username_var.set("")
        if hasattr(self, "password_var"):
            self.password_var.set("")
        if hasattr(self, "url_var"):
            self.url_var.set("")
        if hasattr(self, "note_text"):
            self.note_text.delete("1.0", END)
        self.selected_entry_id = None
        self.is_creating_new_entry = False

    def _toggle_password_visibility(self) -> None:
        self.password_visible = not self.password_visible
        self.password_entry.configure(show="" if self.password_visible else "*")
    def _copy_password(self) -> None:
        password = self.password_var.get()
        if not password:
            Messagebox.show_warning(self._t("nothing_to_copy"), self._t("missing_password_title"))
            return

        pyperclip.copy(password)
        self.last_copied_password = password

        if self.clipboard_clear_job is not None:
            self.after_cancel(self.clipboard_clear_job)

        self.clipboard_clear_job = self.after(
            CLIPBOARD_CLEAR_SECONDS * 1000,
            self._clear_clipboard_if_unchanged,
        )

        Messagebox.show_info(
            f"Heslo bylo zkopírováno do schránky. "
            f"Schránka se automaticky vymaže za {CLIPBOARD_CLEAR_SECONDS} sekund.",
            "Zkopírováno",
        )

    def _clear_clipboard_if_unchanged(self) -> None:
        try:
            current_clipboard = pyperclip.paste()
            if current_clipboard == self.last_copied_password:
                pyperclip.copy("")
        except pyperclip.PyperclipException:
            pass
        finally:
            self.last_copied_password = None
            self.clipboard_clear_job = None

    def _generate_password(self) -> None:
        lowercase_chars = string.ascii_lowercase
        uppercase_chars = string.ascii_uppercase
        digit_chars = string.digits
        special_chars = "!@#$%^&*()-_=+[]{};:,.?"

        all_chars = lowercase_chars + uppercase_chars + digit_chars + special_chars

        password_chars = [
            secrets.choice(lowercase_chars),
            secrets.choice(uppercase_chars),
            secrets.choice(digit_chars),
            secrets.choice(special_chars),
        ]

        password_chars.extend(
            secrets.choice(all_chars)
            for _ in range(24 - len(password_chars))
        )

        secure_random = secrets.SystemRandom()
        secure_random.shuffle(password_chars)

        self.password_var.set("".join(password_chars))


def check_required_dependencies_or_exit() -> None:
    """
    Kontrola kritické závislosti ještě před vytvořením hlavního okna aplikace.
    Bez Argon2id aplikaci nespouštíme, protože by nebylo možné bezpečně
    odvodit šifrovací klíč z hlavního hesla.
    """
    has_crypto = (
        importlib.util.find_spec("cryptography.fernet") is not None
        and importlib.util.find_spec("cryptography.hazmat.primitives.kdf.argon2") is not None
    )

    if not has_crypto:
        system_messagebox.showerror(
            "Chybí požadovaná závislost",
            "Aplikaci nelze spustit.\n\n"
            "Je vyžadována knihovna cryptography s podporou Argon2id.\n\n"
            "Spusť v PowerShellu:\n"
            "py -m pip install --upgrade cryptography ttkbootstrap pyperclip"
        )
        raise SystemExit(1)


if __name__ == "__main__":
    check_required_dependencies_or_exit()
    app = PasswordManagerApp()
    app.mainloop()





