# -*- coding: utf-8 -*-
"""DokoGen — Пользовательский интерфейс (tkinter)"""

import tkinter as tk
from tkinter import ttk, messagebox, filedialog, simpledialog, scrolledtext
import os
import json
import sys
import threading
from datetime import datetime
from typing import List, Optional

from . import (
    Company, CommissionMember, InformationSystem,
    inflect_name,
    build_replacements, VARIABLE_DEFS,
    process_template,
    get_short_fio, get_initials,
)

from .verify import (
    check_declension_morpher, check_company_dadata,
    check_declension_ai, check_company_ai,
    COMPANY_FIELDS, compare_company_values,
)
from .api_settings import load_api_settings, save_api_settings


class DokoGenApp:
    """Главное окно приложения."""

    def __init__(self, root: tk.Tk):
        self.root = root
        root.title("ДоКоГеНеРаТоР — 187-ФЗ (КИИ)")
        root.geometry("1200x800")
        root.minsize(900, 600)
        self._set_app_icon()

        self.model = Company()
        self.current_file: Optional[str] = None
        self._current_is_idx: Optional[int] = None
        self._template_files: List[str] = []
        self._selected_templates: set = set()
        self._last_template_dir: str = ""
        self._autosave_path = os.path.join(
            os.path.dirname(os.path.abspath(__file__)),
            '.autosave', 'autosave_187.json'
        )

        # Путь к файлу настроек импорта (словарь «что и откуда брать»)
        # None = import_settings.json рядом с программой (создаётся автоматически)
        self.import_settings_path: Optional[str] = None

        # Файловый лог: рядом с exe (frozen) или рядом с модулем
        if getattr(sys, 'frozen', False):
            _base = os.path.dirname(sys.executable)
        else:
            _base = os.path.dirname(os.path.abspath(__file__))
        self._log_path = os.path.join(_base, 'dokogen.log')

        # Переменные
        self.doc_number_var = tk.StringVar(value="")
        self.doc_date_var = tk.StringVar(value="")
        self.templates_var = tk.StringVar()
        self.save_var = tk.StringVar()
        self.status_var = tk.StringVar(value="Готов")

        # Словари для хранения виджетов
        self._company_entries: dict = {}
        self._official_entries: dict = {}
        self.commission_entries: list = []

        self._build_menu()
        self._build_ui()
        self._enable_context_menu()
        self._init_company()

        # Привязка закрытия
        root.protocol("WM_DELETE_WINDOW", self._on_close)

        # Автосохранение
        self._load_autosave()
        self._start_autosave_timer()

    def _set_app_icon(self):
        """Устанавливает иконку приложения на окно (заголовок, панель задач).

        Работает и из исходников, и из exe (frozen):
        - исходники: dokogen/icon.ico рядом с модулем
        - exe: dokogen/icon.ico из _MEIPASS (ресурсы PyInstaller)
        """
        try:
            if getattr(sys, 'frozen', False):
                base = getattr(sys, '_MEIPASS', os.path.dirname(sys.executable))
                ico = os.path.join(base, 'dokogen', 'icon.ico')
                if not os.path.exists(ico):
                    ico = os.path.join(os.path.dirname(sys.executable), 'icon.ico')
            else:
                ico = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'icon.ico')
            if os.path.exists(ico):
                self.root.iconbitmap(ico)
        except Exception:
            pass

    # ==================== МЕНЮ ====================
    def _build_menu(self):
        menubar = tk.Menu(self.root)
        self.root.config(menu=menubar)

        file_menu = tk.Menu(menubar, tearoff=0)
        menubar.add_cascade(label="Файл", menu=file_menu)
        file_menu.add_command(label="Новое", command=self.new_data, accelerator="Ctrl+N")
        file_menu.add_command(label="Открыть", command=self.open_data, accelerator="Ctrl+O")
        file_menu.add_command(label="Сохранить", command=self.save_data, accelerator="Ctrl+S")
        file_menu.add_command(label="Сохранить как", command=self.save_as_data, accelerator="Ctrl+Shift+S")
        file_menu.add_separator()

        self.recent_menu = tk.Menu(file_menu, tearoff=0)
        file_menu.add_cascade(label="Последние файлы", menu=self.recent_menu)
        file_menu.add_separator()
        file_menu.add_command(label="Настройки…", command=self.open_api_settings)
        file_menu.add_separator()
        file_menu.add_command(label="Выход", command=self._on_close)

        gen_menu = tk.Menu(menubar, tearoff=0)
        menubar.add_cascade(label="Генерация", menu=gen_menu)
        gen_menu.add_command(label="Импорт из опросника", command=self.import_from_survey)
        gen_menu.add_command(label="Файл настроек импорта…", command=self.select_import_settings)
        gen_menu.add_separator()
        gen_menu.add_command(label="Сгенерировать всё", command=self.generate_all)

        help_menu = tk.Menu(menubar, tearoff=0)
        menubar.add_cascade(label="Справка", menu=help_menu)
        help_menu.add_command(label="О программе", command=self.show_about)
        help_menu.add_command(label="Инструкция пользователя (Word)…", command=self.download_user_manual)
        help_menu.add_command(label="Инструкция администратора (Word)…", command=self.download_admin_manual)
        help_menu.add_command(label="Переменные шаблонов (Word)…", command=self.download_variables_doc)
        help_menu.add_command(label="Пустой опросник (Excel)…", command=self.download_survey)

    # ==================== ИНТЕРФЕЙС ====================
    def _build_ui(self):
        # Панель инструментов
        toolbar = ttk.Frame(self.root)
        toolbar.pack(side=tk.TOP, fill=tk.X, padx=5, pady=3)

        ttk.Button(toolbar, text="📂 Открыть", command=self.open_data).pack(side=tk.LEFT, padx=2)
        ttk.Button(toolbar, text="💾 Сохранить", command=self.save_data).pack(side=tk.LEFT, padx=2)
        ttk.Button(toolbar, text="📥 Импорт", command=self.import_from_survey).pack(side=tk.LEFT, padx=2)
        ttk.Button(toolbar, text="📁 Шаблоны", command=self.select_templates).pack(side=tk.LEFT, padx=2)
        ttk.Button(toolbar, text="⚡ Генерация", command=self.generate_all).pack(side=tk.LEFT, padx=2)

        # Ноутбук (вкладки)
        self.notebook = ttk.Notebook(self.root)
        self.notebook.pack(fill=tk.BOTH, expand=True, padx=5, pady=2)

        self._build_company_tab()
        self._build_is_tab()
        self._build_commission_tab()
        self._build_users_tab()
        self._build_generation_tab()
        self._build_log_tab()

        # Статус-бар
        statusbar = ttk.Label(self.root, textvariable=self.status_var,
                              relief=tk.SUNKEN, anchor=tk.W)
        statusbar.pack(side=tk.BOTTOM, fill=tk.X)

        # Горячие клавиши
        self.root.bind("<Control-n>", lambda e: self.new_data())
        self.root.bind("<Control-o>", lambda e: self.open_data())
        self.root.bind("<Control-s>", lambda e: self.save_data())
        self.root.bind("<Control-S>", lambda e: self.save_as_data())

    # ---------- ВКЛАДКА "ОРГАНИЗАЦИЯ" ----------
    def _build_company_tab(self):
        tab = ttk.Frame(self.notebook)
        self.notebook.add(tab, text="Организация")

        canvas = tk.Canvas(tab, highlightthickness=0)
        scrollbar = ttk.Scrollbar(tab, orient=tk.VERTICAL, command=canvas.yview)
        scrollable = ttk.Frame(canvas)
        scrollable.bind("<Configure>",
                        lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
        canvas.create_window((0, 0), window=scrollable, anchor="nw")
        canvas.configure(yscrollcommand=scrollbar.set)
        canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)

        main = ttk.Frame(scrollable, padding=15)
        main.pack(fill=tk.BOTH, expand=True)

        left = ttk.Frame(main)
        left.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)

        # --- Название организации ---
        f1 = ttk.LabelFrame(left, text="Наименование организации", padding=10)
        f1.pack(fill=tk.X, pady=5)

        ttk.Label(f1, text="Полное название:").grid(row=0, column=0, sticky=tk.W, pady=2)
        self.company_name_entry = ttk.Entry(f1, width=60)
        self.company_name_entry.grid(row=0, column=1, padx=10, sticky=tk.EW)
        ttk.Button(f1, text="Склонять", command=self._update_company_declensions).grid(
            row=0, column=2, padx=5)

        ttk.Label(f1, text="Краткое название:").grid(row=1, column=0, sticky=tk.W, pady=2)
        self.short_name_entry = ttk.Entry(f1, width=60)
        self.short_name_entry.grid(row=1, column=1, padx=10, sticky=tk.EW)
        ttk.Button(f1, text="Склонять", command=self._update_short_declensions).grid(
            row=1, column=2, padx=5)
        f1.columnconfigure(1, weight=1)

        # Склонения полного названия
        decl_frame = ttk.Frame(f1)
        decl_frame.grid(row=2, column=0, columnspan=3, sticky=tk.EW, pady=(5, 0))
        f1.columnconfigure(0, weight=0)
        f1.columnconfigure(1, weight=1)
        f1.columnconfigure(2, weight=0)

        self.decl_entries = {}
        cases = [
            ("Именительный (Кто? Что?)", "nomn"),
            ("Родительный (Кого? Чего?)", "gent"),
            ("Дательный (Кому? Чему?)", "datv"),
            ("Винительный (Кого? Что?)", "accs"),
            ("Творительный (Кем? Чем?)", "ablt"),
            ("Предложный (О ком? О чём?)", "loct"),
        ]
        for i, (label, code) in enumerate(cases):
            ttk.Label(decl_frame, text=label, font=('', 8)).grid(row=i, column=0, sticky=tk.W, padx=(0, 5))
            entry = ttk.Entry(decl_frame, width=55, font=('', 8))
            entry.grid(row=i, column=1, sticky=tk.EW, pady=1)
            self.decl_entries[code] = entry
        decl_frame.columnconfigure(1, weight=1)

        # Склонения краткого названия
        ttk.Label(decl_frame, text="", font=('', 5)).grid(row=6, column=0)
        self.short_decl_entries = {}
        for i, (label, code) in enumerate(cases):
            row = i + 7
            ttk.Label(decl_frame, text=f"Кратк. {label.lower()}",
                      font=('', 8)).grid(row=row, column=0, sticky=tk.W, padx=(0, 5))
            entry = ttk.Entry(decl_frame, width=55, font=('', 8))
            entry.grid(row=row, column=1, sticky=tk.EW, pady=1)
            self.short_decl_entries[code] = entry
        decl_frame.columnconfigure(1, weight=1)

        # --- Адрес и реквизиты ---
        f2 = ttk.LabelFrame(left, text="Адрес и реквизиты", padding=10)
        f2.pack(fill=tk.X, pady=5)

        addr_fields = [
            ("Юридический адрес:", "address"),
            ("Город:", "city_name"),
            ("ИНН:", "inn"),
            ("КПП:", "kpp"),
            ("ОГРН:", "ogrn"),
            ("Дата ОГРН:", "ogrn_date"),
            ("Доменное имя и внешний IP:", "domain_name"),
        ]
        for i, (label, key) in enumerate(addr_fields):
            ttk.Label(f2, text=label).grid(row=i, column=0, sticky=tk.W, pady=2)
            entry = ttk.Entry(f2, width=50)
            entry.grid(row=i, column=1, padx=10, sticky=tk.EW, pady=1)
            self._company_entries[key] = entry

        # Привязка автозаполнения города из адреса
        addr_entry = self._company_entries.get('address')
        if addr_entry:
            addr_entry.bind('<KeyRelease>', self._on_address_changed)

        f2.columnconfigure(1, weight=1)

        # --- ОКВЭД ---
        f_okved = ttk.LabelFrame(left, text="ОКВЭД", padding=10)
        f_okved.pack(fill=tk.X, pady=5)

        # Основной ОКВЭД — один, из словаря или вручную
        ttk.Label(f_okved, text="Основной ОКВЭД:").grid(row=0, column=0, sticky=tk.W, pady=2)
        okved_entry = ttk.Entry(f_okved, width=50)
        okved_entry.grid(row=0, column=1, padx=10, sticky=tk.EW, pady=1)
        self._company_entries['okved'] = okved_entry
        ttk.Button(f_okved, text="Выбрать из словаря",
                   command=self._select_main_okved).grid(row=0, column=2, padx=5)

        # Дополнительные ОКВЭД — сколько угодно
        ttk.Label(f_okved, text="Дополнительные ОКВЭД:").grid(row=1, column=0, sticky=tk.NW, pady=2)
        self.okved_extra_listbox = tk.Listbox(f_okved, height=4, width=50)
        self.okved_extra_listbox.grid(row=1, column=1, padx=10, sticky=tk.EW, pady=1)
        okved_btn = ttk.Frame(f_okved)
        okved_btn.grid(row=1, column=2, padx=5, sticky=tk.N)
        ttk.Button(okved_btn, text="Выбрать из словаря",
                   command=self._select_extra_okved).pack(fill=tk.X, pady=1)
        ttk.Button(okved_btn, text="+ Добавить",
                   command=self._add_okved_manual).pack(fill=tk.X, pady=1)
        ttk.Button(okved_btn, text="- Удалить",
                   command=self._delete_okved_extra).pack(fill=tk.X, pady=1)

        f_okved.columnconfigure(1, weight=1)

        # --- Должностные лица ---
        f3 = ttk.LabelFrame(left, text="Должностные лица", padding=10)
        f3.pack(fill=tk.X, pady=5)

        of_fields = [
            ("Руководитель (должность):", "chief_position"),
            ("Руководитель (ФИО):", "chief_fio"),
            ("Ответственный (должность):", "responsible_position"),
            ("Ответственный (ФИО):", "responsible_fio"),
            ("Администратор (должность):", "admin_position"),
            ("Администратор (ФИО):", "admin_fio"),
        ]
        for i, (label, key) in enumerate(of_fields):
            ttk.Label(f3, text=label).grid(row=i, column=0, sticky=tk.W, pady=2)
            entry = ttk.Entry(f3, width=50)
            entry.grid(row=i, column=1, padx=10, sticky=tk.EW, pady=1)
            self._official_entries[key] = entry
        f3.columnconfigure(1, weight=1)

        # --- Документ ---
        f4 = ttk.LabelFrame(left, text="Документ", padding=10)
        f4.pack(fill=tk.X, pady=5)
        ttk.Label(f4, text="Номер договора:").grid(row=0, column=0, sticky=tk.W)
        ttk.Entry(f4, textvariable=self.doc_number_var, width=25).grid(
            row=0, column=1, padx=10, sticky=tk.W)
        ttk.Label(f4, text="Дата договора:").grid(row=1, column=0, sticky=tk.W)
        ttk.Entry(f4, textvariable=self.doc_date_var, width=25).grid(
            row=1, column=1, padx=10, sticky=tk.W)

        # --- Финансовые показатели (опросник) ---
        f_fin = ttk.LabelFrame(left, text="Финансовые показатели", padding=10)
        f_fin.pack(fill=tk.X, pady=5)
        fin_fields = [
            ("Сумма прибыли за предыдущий год (руб.):", "profit"),
            ("Сумма налога с прибыли (руб.):", "profit_tax"),
        ]
        for i, (label, key) in enumerate(fin_fields):
            ttk.Label(f_fin, text=label).grid(row=i, column=0, sticky=tk.W, pady=2)
            entry = ttk.Entry(f_fin, width=50)
            entry.grid(row=i, column=1, padx=10, sticky=tk.EW, pady=1)
            self._company_entries[key] = entry
        f_fin.columnconfigure(1, weight=1)

        # --- Правая панель: проверка данных ---
        right = ttk.LabelFrame(main, text="Проверка данных", padding=10)
        right.pack(side=tk.RIGHT, fill=tk.Y, padx=(10, 0))

        ttk.Label(right, text="Склонение названия:", font=('', 9, 'bold')).pack(anchor=tk.W)
        ttk.Button(right, text="🌐 Склонение — Морфер 3.0",
                   command=self._check_declension_web).pack(fill=tk.X, pady=2)
        ttk.Button(right, text="🤖 Склонение — ИИ",
                   command=self._check_declension_ai).pack(fill=tk.X, pady=2)

        ttk.Label(right, text="Реквизиты организации:", font=('', 9, 'bold')).pack(anchor=tk.W, pady=(10, 0))
        ttk.Button(right, text="🌐 Реквизиты — DaData",
                   command=self._check_company_web).pack(fill=tk.X, pady=2)
        ttk.Button(right, text="🤖 Реквизиты — ИИ",
                   command=self._check_company_ai).pack(fill=tk.X, pady=2)

        ttk.Label(right, text="Результат проверки:", font=('', 9, 'bold')).pack(anchor=tk.W, pady=(10, 0))
        self._verify_text = scrolledtext.ScrolledText(
            right, width=44, height=14, state=tk.DISABLED, wrap=tk.WORD, font=('', 9))
        self._verify_text.pack(fill=tk.BOTH, expand=True, pady=4)

        btn_row = ttk.Frame(right)
        btn_row.pack(fill=tk.X)
        ttk.Button(btn_row, text="✓ Применить", command=self._verify_apply).pack(side=tk.LEFT, padx=2)
        ttk.Button(btn_row, text="Очистить", command=self._verify_clear).pack(side=tk.LEFT, padx=2)

        # Выборочное применение: чекбоксы «что подставлять»
        self._apply_flags = {
            'declension': tk.BooleanVar(value=True),
            'short_declension': tk.BooleanVar(value=True),
            'address': tk.BooleanVar(value=True),
            'requisites': tk.BooleanVar(value=True),
            'chief': tk.BooleanVar(value=True),
            'okved': tk.BooleanVar(value=True),
            'okved_extra': tk.BooleanVar(value=True),
        }
        apply_row = ttk.Frame(right)
        apply_row.pack(fill=tk.X, pady=(4, 0))
        ttk.Label(apply_row, text="Подставлять:", font=('', 8)).pack(side=tk.LEFT, padx=(0, 4))
        ttk.Checkbutton(apply_row, text="склонения", variable=self._apply_flags['declension']).pack(side=tk.LEFT)
        ttk.Checkbutton(apply_row, text="кр. склон.", variable=self._apply_flags['short_declension']).pack(side=tk.LEFT)
        ttk.Checkbutton(apply_row, text="адрес", variable=self._apply_flags['address']).pack(side=tk.LEFT)
        ttk.Checkbutton(apply_row, text="ИНН/КПП/ОГРН", variable=self._apply_flags['requisites']).pack(side=tk.LEFT)
        ttk.Checkbutton(apply_row, text="руковод.", variable=self._apply_flags['chief']).pack(side=tk.LEFT)
        apply_row2 = ttk.Frame(right)
        apply_row2.pack(fill=tk.X, pady=(2, 0))
        ttk.Label(apply_row2, text="", font=('', 8), width=10).pack(side=tk.LEFT)
        ttk.Checkbutton(apply_row2, text="ОКВЭД осн.", variable=self._apply_flags['okved']).pack(side=tk.LEFT)
        ttk.Checkbutton(apply_row2, text="ОКВЭД доп.", variable=self._apply_flags['okved_extra']).pack(side=tk.LEFT)
        self._verify_result = None

    # ---------- ПРОВЕРКА ДАННЫХ (Морфер / DaData / ИИ) ----------
    def open_api_settings(self):
        """Окно настроек API-ключей (меню «Файл → Настройки»)."""
        win = tk.Toplevel(self.root)
        win.title("Настройки")
        win.geometry("540x460")
        win.transient(self.root)
        win.grab_set()

        nb = ttk.Notebook(win)
        nb.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)

        # Вкладка API-ключи
        api_tab = ttk.Frame(nb, padding=10)
        nb.add(api_tab, text="API-ключи")

        settings = load_api_settings()

        ttk.Label(api_tab, text="DaData — проверка организации по ИНН/ОГРН",
                  font=('', 9, 'bold')).pack(anchor=tk.W)
        ttk.Label(api_tab, text="API-ключ (получить на https://dadata.ru):").pack(anchor=tk.W, pady=(6, 0))
        dadata_var = tk.StringVar(value=settings.get('dadata_token', ''))
        ttk.Entry(api_tab, textvariable=dadata_var, width=64).pack(fill=tk.X, pady=2)

        ttk.Label(api_tab, text="ИИ-проверка (OpenAI-совместимый API: DeepSeek, ChatGPT и др.)",
                  font=('', 9, 'bold')).pack(anchor=tk.W, pady=(14, 0))
        ttk.Label(api_tab, text="API-ключ:").pack(anchor=tk.W, pady=(6, 0))
        ds_var = tk.StringVar(value=settings.get('ai_api_key', ''))
        ttk.Entry(api_tab, textvariable=ds_var, width=64).pack(fill=tk.X, pady=2)
        ttk.Label(api_tab, text="Модель (например: deepseek-chat, gpt-4o-mini, gpt-4o):").pack(anchor=tk.W, pady=(6, 0))
        model_var = tk.StringVar(value=settings.get('ai_model', 'deepseek-chat'))
        ttk.Entry(api_tab, textvariable=model_var, width=64).pack(fill=tk.X, pady=2)
        ttk.Label(api_tab, text="Базовый URL API (для DeepSeek можно оставить пустым):").pack(anchor=tk.W, pady=(6, 0))
        url_var = tk.StringVar(value=settings.get('ai_base_url', ''))
        ttk.Entry(api_tab, textvariable=url_var, width=64).pack(fill=tk.X, pady=2)

        ttk.Label(api_tab, text="Морфер 3.0 работает без ключа (бесплатный склонятор).",
                  foreground='gray').pack(anchor=tk.W, pady=(14, 0))

        def _save():
            save_api_settings({
                'dadata_token': dadata_var.get().strip(),
                'ai_api_key': ds_var.get().strip(),
                'ai_model': model_var.get().strip() or 'deepseek-chat',
                'ai_base_url': url_var.get().strip(),
            })
            self.log("✅ Настройки API сохранены")
            win.destroy()

        ttk.Button(api_tab, text="💾 Сохранить", command=_save).pack(anchor=tk.W, pady=12)

    # ---------- ПРОВЕРКИ ----------
    _CASE_TO_CODE = {'И': 'nomn', 'Р': 'gent', 'Д': 'datv', 'В': 'accs', 'Т': 'ablt', 'П': 'loct'}

    def _verify_log(self, text):
        self._verify_text.config(state=tk.NORMAL)
        self._verify_text.delete('1.0', tk.END)
        self._verify_text.insert('1.0', text)
        self._verify_text.config(state=tk.DISABLED)

    def _verify_clear(self):
        self._verify_result = None
        self._verify_log('')

    def _check_declension_web(self):
        name = self.company_name_entry.get().strip()
        short = self.short_name_entry.get().strip()
        if not name and not short:
            messagebox.showwarning("Проверка", "Сначала введите полное или краткое название организации")
            return
        self.log(f"🌐 Запрос к Морфер 3.0: «{name or short}»")
        self._verify_log("Запрос к Морфер 3.0…")

        def _run():
            # Склоняем и полное, и краткое название (если заполнено)
            results = []
            if name:
                r1, e1 = check_declension_morpher(name)
                if e1:
                    self.root.after(0, lambda: self._verify_log(f"❌ {e1}"))
                    return
                results.append(('declension', r1))
            if short:
                r2, e2 = check_declension_morpher(short)
                if e2:
                    self.root.after(0, lambda: self._verify_log(f"❌ Краткое: {e2}"))
                    return
                results.append(('short_declension', r2))
            self.root.after(0, lambda: self._verify_declension_done(results, None))

        threading.Thread(target=_run, daemon=True).start()

    def _check_declension_ai(self):
        name = self.company_name_entry.get().strip()
        short = self.short_name_entry.get().strip()
        if not name and not short:
            messagebox.showwarning("Проверка", "Сначала введите полное или краткое название организации")
            return
        settings = load_api_settings()
        if not settings.get('ai_api_key'):
            messagebox.showwarning("Настройки", "Не задан API-ключ ИИ.\nМеню «Файл → Настройки»")
            return
        self.log(f"🤖 Запрос к ИИ ({settings.get('ai_model') or 'deepseek-chat'}): склонение «{name or short}»")
        self._verify_log("Запрос к ИИ…")

        def _run():
            results = []
            if name:
                r1, e1 = check_declension_ai(name, settings['ai_api_key'],
                                             settings.get('ai_model'), settings.get('ai_base_url'))
                if e1:
                    self.root.after(0, lambda: self._verify_log(f"❌ {e1}"))
                    return
                results.append(('declension', r1))
            if short:
                r2, e2 = check_declension_ai(short, settings['ai_api_key'],
                                             settings.get('ai_model'), settings.get('ai_base_url'))
                if e2:
                    self.root.after(0, lambda: self._verify_log(f"❌ Краткое: {e2}"))
                    return
                results.append(('short_declension', r2))
            self.root.after(0, lambda: self._verify_declension_done(results, None))

        threading.Thread(target=_run, daemon=True).start()

    def _verify_declension_done(self, results, err):
        if err:
            self._verify_log(f"❌ {err}")
            self.log(f"❌ Проверка склонений: {err}")
            return
        # Собираем единый результат для применения
        combined = {
            'source': results[0][1]['source'] if results else 'ИИ',
            'source_url': results[0][1]['source_url'] if results else '',
            'disclaimer': results[0][1].get('disclaimer', '') if results else '',
            'kind': 'declension',
            'data': {},
            'apply': {},
        }
        lines = [f"Источник: {combined['source']} ({combined['source_url']})"]
        if combined['disclaimer']:
            lines.append(combined['disclaimer'])
        for key, res in results:
            label = "Полное название" if key == 'declension' else "Краткое название"
            lines.append(f"\n{label}:")
            for code, name_case in [('И', 'Именительный'), ('Р', 'Родительный'), ('Д', 'Дательный'), ('В', 'Винительный'),
                                    ('Т', 'Творительный'), ('П', 'Предложный')]:
                if code in res['data']:
                    lines.append(f"  {name_case}: {res['data'][code]}")
            if res.get('comment'):
                lines.append(f"  Комментарий: {res['comment']}")
            combined['apply'][key] = res['apply']['declension']
        lines.append("\nНажмите «✓ Применить», чтобы подставить склонения")
        self._verify_result = combined
        self._verify_log('\n'.join(lines))
        # Общий лог с ответом сервиса
        sample = ''
        for key, res in results:
            sample = res['data'].get('П', '')
            if sample:
                break
        self.log(f"{combined['source']}: склонение проверено → «{sample}»")

    def _check_company_web(self):
        inn_entry = self._company_entries.get('inn')
        inn = inn_entry.get().strip() if inn_entry else ''
        if not inn:
            messagebox.showwarning("Проверка", "Сначала введите ИНН организации")
            return
        settings = load_api_settings()
        if not settings.get('dadata_token'):
            messagebox.showwarning("Настройки", "Не задан API-ключ DaData.\nМеню «Файл → Настройки»")
            return
        self.log(f"🌐 Запрос к DaData: ИНН/ОГРН {inn}")
        self._verify_log("Запрос к DaData…")

        def _run():
            result, err = check_company_dadata(inn, settings['dadata_token'])
            self.root.after(0, lambda: self._verify_company_done(result, err))

        threading.Thread(target=_run, daemon=True).start()

    def _check_company_ai(self):
        self._collect_company()
        c = self.model
        settings = load_api_settings()
        if not settings.get('ai_api_key'):
            messagebox.showwarning("Настройки", "Не задан API-ключ ИИ.\nМеню «Файл → Настройки»")
            return
        company_data = {
            'full_name': c.full_name, 'short_name': c.short_name,
            'address': c.address, 'inn': c.inn, 'kpp': c.kpp, 'ogrn': c.ogrn,
            'ogrn_date': c.ogrn_date,
            'chief_position': c.chief_position, 'chief_fio': c.chief_fio,
        }
        self.log(f"🤖 Запрос к ИИ ({settings.get('ai_model') or 'deepseek-chat'}): реквизиты (ИНН {company_data.get('inn') or '—'})")
        self._verify_log("Запрос к ИИ…")

        def _run():
            result, err = check_company_ai(company_data, settings['ai_api_key'],
                                           settings.get('ai_model'), settings.get('ai_base_url'))
            self.root.after(0, lambda: self._verify_company_done(result, err))

        threading.Thread(target=_run, daemon=True).start()

    def _verify_company_done(self, result, err):
        if err:
            self._verify_log(f"❌ {err}")
            self.log(f"❌ Проверка реквизитов: {err}")
            return
        self._verify_result = result
        lines = [f"Источник: {result['source']} ({result['source_url']})"]
        if result.get('disclaimer'):
            lines.append(result['disclaimer'])
        d = result['data']
        if 'full_name' in d:
            # DaData — фактические данные
            for key, label in [('full_name', 'Наименование'), ('short_name', 'Краткое'),
                               ('address', 'Адрес'), ('inn', 'ИНН'), ('kpp', 'КПП'),
                               ('ogrn', 'ОГРН'), ('ogrn_date', 'Дата ОГРН'),
                               ('okved', 'Основной ОКВЭД'),
                               ('chief_position', 'Руководитель'), ('chief_fio', 'ФИО')]:
                if d.get(key):
                    lines.append(f"{label}: {d[key]}")
            if d.get('okved_extra'):
                lines.append(f"Доп. ОКВЭД: {', '.join(d['okved_extra'])}")
        else:
            # ИИ-рецензия: показываем снимок полей (что ИИ увидел), замечания и предложения
            lines.append(f"Оценка: {d.get('rating', '')}")
            fields = d.get('fields') or {}
            if fields:
                lines.append("\nПроверенные поля:")
                for key, label in COMPANY_FIELDS:
                    info = fields.get(key)
                    if isinstance(info, dict):
                        status = info.get('статус', '')
                        comment = info.get('комментарий', '')
                        lines.append(f"  {label}: {status}" + (f" — {comment}" if comment else ""))
            for n in (d.get('notes') or []):
                lines.append(f"• {n}")
            props = d.get('proposals') or {}
            if any(props.values()):
                lines.append("\nПредложения ИИ:")
                for key, label in COMPANY_FIELDS:
                    v = props.get(key)
                    if v:
                        lines.append(f"  {label}: {v}")
        lines.append("\nНажмите «✓ Применить», чтобы подставить данные")
        self._verify_log('\n'.join(lines))

        # Сравнение полученных значений с введёнными (серьёзные расхождения — красным)
        current = {
            'full_name': self.model.full_name,
            'short_name': self.model.short_name,
            'address': self.model.address,
            'inn': self.model.inn,
            'kpp': self.model.kpp,
            'ogrn': self.model.ogrn,
            'ogrn_date': self.model.ogrn_date,
            'chief_position': self.model.chief_position,
            'chief_fio': self.model.chief_fio,
        }
        if 'full_name' in d:
            new_vals = d  # DaData — фактические данные
        else:
            new_vals = d.get('proposals') or {}  # ИИ — предложения
        diffs = compare_company_values(current, new_vals)
        if diffs:
            warn = "⚠ ВНИМАНИЕ! НАЙДЕНЫ РАСХОЖДЕНИЯ:\n"
            for label, old_v, new_v in diffs:
                warn += f"• {label}: было «{old_v}», стало «{new_v}»\n"
            self._verify_text.tag_configure('warn', foreground='red',
                                            font=('Arial', 14, 'bold'))
            self._verify_text.config(state=tk.NORMAL)
            self._verify_text.insert(tk.END, '\n' + warn, 'warn')
            self._verify_text.config(state=tk.DISABLED)
            self.log(f"⚠ ВНИМАНИЕ: найдены расхождения ({len(diffs)}): " +
                     '; '.join(f"{l}: {o} → {n}" for l, o, n in diffs[:3]))

        # Общий лог с ответом сервиса
        summary = ''
        if 'full_name' in d:
            summary = f"{d.get('full_name', '')[:60]} (ИНН {d.get('inn', '')})"
        else:
            summary = f"оценка {d.get('rating', '')}, замечаний: {len(d.get('notes') or [])}"
        self.log(f"{result['source']}: проверка реквизитов → {summary}")

    def _verify_apply(self):
        result = self._verify_result
        if not result:
            return
        apply_data = result.get('apply', {})
        flags = getattr(self, '_apply_flags', None)

        def want(key):
            if flags is None:
                return True
            return flags[key].get() if key in flags else True

        idx_map = {'nomn': 1, 'gent': 2, 'datv': 3, 'accs': 4, 'ablt': 5, 'loct': 6}
        if 'declension' in apply_data and want('declension'):
            for case, val in apply_data['declension'].items():
                code = self._CASE_TO_CODE.get(case)
                if code and code in self.decl_entries:
                    self.decl_entries[code].delete(0, tk.END)
                    self.decl_entries[code].insert(0, val)
                    setattr(self.model, f'company_name_{idx_map[code]}', val)
            self.log("✅ Склонения полного названия применены")
        if 'short_declension' in apply_data and want('short_declension'):
            for case, val in apply_data['short_declension'].items():
                code = self._CASE_TO_CODE.get(case)
                if code and code in self.short_decl_entries:
                    self.short_decl_entries[code].delete(0, tk.END)
                    self.short_decl_entries[code].insert(0, val)
                    setattr(self.model, f'short_name_{idx_map[code]}', val)
            self.log("✅ Склонения краткого названия применены")
        if 'company' in apply_data:
            comp = apply_data['company']
            if comp.get('full_name'):
                self.company_name_entry.delete(0, tk.END)
                self.company_name_entry.insert(0, comp['full_name'])
            if comp.get('short_name'):
                self.short_name_entry.delete(0, tk.END)
                self.short_name_entry.insert(0, comp['short_name'])
            if want('address') and comp.get('address'):
                if 'address' in self._company_entries:
                    self._company_entries['address'].delete(0, tk.END)
                    self._company_entries['address'].insert(0, comp['address'])
            if want('requisites'):
                for data_key, ui_key in {'kpp': 'kpp', 'ogrn': 'ogrn',
                                         'ogrn_date': 'ogrn_date', 'inn': 'inn'}.items():
                    if comp.get(data_key) and ui_key in self._company_entries:
                        self._company_entries[ui_key].delete(0, tk.END)
                        self._company_entries[ui_key].insert(0, comp[data_key])
                # ОКВЭД из DaData (с названием из словаря/интернета)
                from .verify import resolve_okved
                from .api_settings import load_api_settings as _las3
                _tok3 = (_las3().get('dadata_token') or '')
                if want('okved') and comp.get('okved') and 'okved' in self._company_entries:
                    self._company_entries['okved'].delete(0, tk.END)
                    self._company_entries['okved'].insert(0, resolve_okved(comp['okved'], _tok3))
                if want('okved_extra') and comp.get('okved_extra') and hasattr(self, 'okved_extra_listbox'):
                    self.okved_extra_listbox.delete(0, tk.END)
                    for code in comp['okved_extra']:
                        if str(code).strip():
                            self.okved_extra_listbox.insert(tk.END, resolve_okved(str(code).strip(), _tok3))
            if want('chief'):
                if comp.get('chief_position') and 'chief_position' in self._official_entries:
                    self._official_entries['chief_position'].delete(0, tk.END)
                    self._official_entries['chief_position'].insert(0, comp['chief_position'])
                if comp.get('chief_fio') and 'chief_fio' in self._official_entries:
                    self._official_entries['chief_fio'].delete(0, tk.END)
                    self._official_entries['chief_fio'].insert(0, comp['chief_fio'])
            self.log("✅ Реквизиты применены")

    # ---------- ВКЛАДКА "ИНФОРМАЦИОННЫЕ СИСТЕМЫ" ----------
    def _build_is_tab(self):
        tab = ttk.Frame(self.notebook)
        self.notebook.add(tab, text="Объекты КИИ")

        # Левая панель — список объектов КИИ
        left = ttk.Frame(tab)
        left.pack(side=tk.LEFT, fill=tk.Y, padx=5, pady=5)
        ttk.Label(left, text="Объекты КИИ:").pack()
        self.is_listbox = tk.Listbox(left, width=35, height=18, exportselection=False)
        self.is_listbox.pack(fill=tk.BOTH, expand=True, pady=5)
        self.is_listbox.bind('<<ListboxSelect>>', self._on_is_select)

        btn_frame = ttk.Frame(left)
        btn_frame.pack(fill=tk.X)
        ttk.Button(btn_frame, text="+ Добавить", command=self._add_is).pack(side=tk.LEFT, padx=2)
        ttk.Button(btn_frame, text="Копировать", command=self._copy_is).pack(side=tk.LEFT, padx=2)
        ttk.Button(btn_frame, text="- Удалить", command=self._delete_is).pack(side=tk.LEFT, padx=2)

        # Правая панель — редактирование
        self._is_edit_panel = ttk.LabelFrame(tab, text="Редактирование объекта КИИ")
        self._is_edit_panel.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5)

        fields_frame = ttk.Frame(self._is_edit_panel, padding=10)
        fields_frame.pack(fill=tk.X)

        # Основные поля (сверху — только название, описание, категория)
        ttk.Label(fields_frame, text="Наименование:").grid(row=0, column=0, sticky=tk.W, pady=2)
        self.is_name_entry = ttk.Entry(fields_frame, width=60)
        self.is_name_entry.grid(row=0, column=1, padx=10, sticky=tk.EW)

        ttk.Label(fields_frame, text="Описание:").grid(row=1, column=0, sticky=tk.W, pady=2)
        self.is_description_entry = ttk.Entry(fields_frame, width=60)
        self.is_description_entry.grid(row=1, column=1, padx=10, sticky=tk.EW)

        # Категория значимости объекта КИИ
        self.is_category_var = tk.StringVar(value="без категории")
        ttk.Label(fields_frame, text="Категория значимости:").grid(row=2, column=0, sticky=tk.W, pady=2)
        cat_frame = ttk.Frame(fields_frame)
        cat_frame.grid(row=2, column=1, sticky=tk.W, padx=10)
        for val in ["1", "2", "3", "без категории"]:
            ttk.Radiobutton(cat_frame, text=val, variable=self.is_category_var,
                            value=val).pack(side=tk.LEFT, padx=3)

        ttk.Label(fields_frame, text="Балансовая стоимость (руб.):").grid(
            row=3, column=0, sticky=tk.W, pady=2)
        self.is_balance_entry = ttk.Entry(fields_frame, width=60)
        self.is_balance_entry.grid(row=3, column=1, padx=10, sticky=tk.EW, pady=2)

        fields_frame.columnconfigure(1, weight=1)

        # ---- Вложенные вкладки ----
        nb2 = ttk.Notebook(self._is_edit_panel)
        nb2.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)

        # ===== Вкладка «Основное» =====
        main_tab = ttk.Frame(nb2)
        nb2.add(main_tab, text="Основное")
        main_frame = ttk.Frame(main_tab, padding=8)
        main_frame.pack(fill=tk.BOTH, expand=True)

        # Назначение объекта
        ttk.Label(main_frame, text="Назначение объекта:").grid(row=0, column=0, sticky=tk.W, pady=2)
        self.is_purpose_entry = ttk.Entry(main_frame, width=70)
        self.is_purpose_entry.grid(row=0, column=1, padx=8, sticky=tk.EW, pady=1)

        # Сфера деятельности — словарик + выпадающий список
        ttk.Label(main_frame, text="Сфера деятельности:").grid(row=1, column=0, sticky=tk.W, pady=2)
        self.is_sphere_combo = ttk.Combobox(main_frame, width=68, state='readonly')
        self.is_sphere_combo.grid(row=1, column=1, padx=8, sticky=tk.EW, pady=1)
        try:
            import json as _json
            _sp = os.path.join(os.path.dirname(__file__), 'dictionaries', 'spheres.json')
            with open(_sp, encoding='utf-8') as _f:
                self.is_sphere_combo['values'] = [''] + _json.load(_f)
        except Exception:
            pass

        # Тип объекта — словарик + выпадающий список
        ttk.Label(main_frame, text="Тип объекта:").grid(row=2, column=0, sticky=tk.W, pady=2)
        self.is_object_type_combo = ttk.Combobox(main_frame, width=68, state='readonly')
        self.is_object_type_combo.grid(row=2, column=1, padx=8, sticky=tk.EW, pady=1)
        try:
            _ot = os.path.join(os.path.dirname(__file__), 'dictionaries', 'object_types.json')
            with open(_ot, encoding='utf-8') as _f:
                self.is_object_type_combo['values'] = [''] + _json.load(_f)
        except Exception:
            pass

        # Архитектура объекта — словарик + выпадающий список
        ttk.Label(main_frame, text="Архитектура объекта:").grid(row=3, column=0, sticky=tk.W, pady=2)
        self.is_architecture_combo = ttk.Combobox(main_frame, width=68, state='readonly')
        self.is_architecture_combo.grid(row=3, column=1, padx=8, sticky=tk.EW, pady=1)
        try:
            _ar = os.path.join(os.path.dirname(__file__), 'dictionaries', 'architectures.json')
            with open(_ar, encoding='utf-8') as _f:
                self.is_architecture_combo['values'] = [''] + _json.load(_f)
        except Exception:
            pass

        # Типовой отраслевой объект КИИ — словарик + выпадающий список
        ttk.Label(main_frame, text="Типовой отраслевой объект КИИ:").grid(row=4, column=0, sticky=tk.W, pady=2)
        self.is_typical_object_combo = ttk.Combobox(main_frame, width=68, state='readonly')
        self.is_typical_object_combo.grid(row=4, column=1, padx=8, sticky=tk.EW, pady=1)
        try:
            _to = os.path.join(os.path.dirname(__file__), 'dictionaries', 'typical_objects.json')
            with open(_to, encoding='utf-8') as _f:
                self.is_typical_object_combo['values'] = [''] + _json.load(_f)
        except Exception:
            pass

        main_frame.columnconfigure(1, weight=1)

        # ===== Вкладка «Субъект» =====
        subj_tab = ttk.Frame(nb2)
        nb2.add(subj_tab, text="Субъект")
        subj_frame = ttk.Frame(subj_tab, padding=8)
        subj_frame.pack(fill=tk.BOTH, expand=True)
        subj_fields = [
            ("Наименование организации, эксплуатирующей объект:", "is_subject_name_entry"),
            ("Адрес местонахождения:", "is_subject_address_entry"),
            ("Элемент (компонент) объекта, который эксплуатируется лицом:", "is_subject_element_entry"),
            ("ИНН лица, эксплуатирующего объект:", "is_subject_inn_entry"),
            ("КПП его обособленных подразделений:", "is_subject_kpp_entry"),
        ]
        for i, (label, attr) in enumerate(subj_fields):
            ttk.Label(subj_frame, text=label).grid(row=i, column=0, sticky=tk.W, pady=2)
            entry = ttk.Entry(subj_frame, width=70)
            entry.grid(row=i, column=1, padx=8, sticky=tk.EW, pady=1)
            setattr(self, attr, entry)
        ttk.Button(subj_frame, text="📋 Заполнить из данных организации",
                   command=self._fill_subject_from_company).grid(
            row=len(subj_fields), column=0, columnspan=2, sticky=tk.W, pady=(8, 0))
        subj_frame.columnconfigure(1, weight=1)

        # ===== Вкладка «Сеть» =====
        net_tab = ttk.Frame(nb2)
        nb2.add(net_tab, text="Сеть")
        net_frame = ttk.Frame(net_tab, padding=8)
        net_frame.pack(fill=tk.BOTH, expand=True)

        # Категория сети — словарик
        ttk.Label(net_frame, text="Категория сети электросвязи:").grid(row=1, column=0, sticky=tk.W, pady=2)
        self.is_net_category_combo = ttk.Combobox(net_frame, width=68, state='readonly')
        self.is_net_category_combo.grid(row=1, column=1, padx=8, sticky=tk.EW, pady=1)
        try:
            import json as _json
            _nc = os.path.join(os.path.dirname(__file__), 'dictionaries', 'net_categories.json')
            with open(_nc, encoding='utf-8') as _f:
                self.is_net_category_combo['values'] = [''] + _json.load(_f)
        except Exception:
            pass

        # Провайдер
        ttk.Label(net_frame, text="Наименование провайдера:").grid(row=2, column=0, sticky=tk.W, pady=2)
        self.is_net_operator_entry = ttk.Entry(net_frame, width=70)
        self.is_net_operator_entry.grid(row=2, column=1, padx=8, sticky=tk.EW, pady=1)

        # Цель взаимодействия — словарик
        ttk.Label(net_frame, text="Цель взаимодействия с сетью электросвязи:").grid(row=3, column=0, sticky=tk.W, pady=2)
        self.is_net_purpose_combo = ttk.Combobox(net_frame, width=68, state='readonly')
        self.is_net_purpose_combo.grid(row=3, column=1, padx=8, sticky=tk.EW, pady=1)
        try:
            _np = os.path.join(os.path.dirname(__file__), 'dictionaries', 'net_purposes.json')
            with open(_np, encoding='utf-8') as _f:
                self.is_net_purpose_combo['values'] = [''] + _json.load(_f)
        except Exception:
            pass

        # Способ взаимодействия — словарик
        ttk.Label(net_frame, text="Способ взаимодействия (тип доступа):").grid(row=4, column=0, sticky=tk.W, pady=2)
        self.is_net_method_combo = ttk.Combobox(net_frame, width=68, state='readonly')
        self.is_net_method_combo.grid(row=4, column=1, padx=8, sticky=tk.EW, pady=1)
        try:
            _nm = os.path.join(os.path.dirname(__file__), 'dictionaries', 'net_methods.json')
            with open(_nm, encoding='utf-8') as _f:
                self.is_net_method_combo['values'] = [''] + _json.load(_f)
        except Exception:
            pass

        net_frame.columnconfigure(1, weight=1)

        # ===== Вкладка «ПО» (списки, как «Средства защиты») =====
        sw_tab = ttk.Frame(nb2)
        nb2.add(sw_tab, text="ПО")
        sw_frame = ttk.Frame(sw_tab, padding=8)
        sw_frame.pack(fill=tk.BOTH, expand=True)

        sw_fields = [
            ("ПО и аппаратные средства (модели, производители, количество):", "is_hw_sw_listbox", "hw_sw.json"),
            ("Операционные системы:", "is_os_sw_listbox", "os_sw.json"),
            ("Прикладные программы (MS Office, OpenOffice и пр.):", "is_app_sw_listbox", "app_sw.json"),
        ]
        for i, (label, attr, dict_name) in enumerate(sw_fields):
            row = i * 2
            ttk.Label(sw_frame, text=label).grid(row=row, column=0, sticky=tk.W, pady=(4, 0))
            box = tk.Listbox(sw_frame, height=4, exportselection=False)
            box.grid(row=row + 1, column=0, sticky=tk.EW, padx=(0, 4), pady=1)
            sb = ttk.Scrollbar(sw_frame, orient=tk.VERTICAL, command=box.yview)
            sb.grid(row=row + 1, column=1, sticky=tk.NS)
            box.configure(yscrollcommand=sb.set)
            btn = ttk.Frame(sw_frame)
            btn.grid(row=row + 1, column=2, sticky=tk.N, padx=(4, 0))
            ttk.Button(btn, text="+ Добавить",
                       command=lambda a=attr: self._add_sw_item(getattr(self, a))).pack(fill=tk.X, pady=1)
            ttk.Button(btn, text="- Удалить",
                       command=lambda a=attr: self._del_sw_item(getattr(self, a))).pack(fill=tk.X, pady=1)
            ttk.Button(btn, text="✏️ Редактировать",
                       command=lambda a=attr: self._edit_sw_item(getattr(self, a))).pack(fill=tk.X, pady=1)
            ttk.Button(btn, text="📚 Из словаря",
                       command=lambda a=attr, d=dict_name: self._select_sw_from_dict(getattr(self, a), d, allow_refine=True)).pack(fill=tk.X, pady=1)
            setattr(self, attr, box)
        sw_frame.columnconfigure(0, weight=1)

        # ===== Вкладка «Безопасность» =====
        sec_tab = ttk.Frame(nb2)
        nb2.add(sec_tab, text="Безопасность")
        sec_frame = ttk.Frame(sec_tab, padding=6)
        sec_frame.pack(fill=tk.BOTH, expand=True)

        # --- Панель действий (сверху, всегда видна) ---
        sec_actions = ttk.Frame(sec_frame)
        sec_actions.pack(fill=tk.X, pady=(0, 6))
        ttk.Button(sec_actions, text="⬇️ Загрузить угрозы ФСТЭК",
                   command=self._download_fstec_threats).pack(side=tk.LEFT, padx=2)
        ttk.Button(sec_actions, text="⚙️ Подобрать угрозы (программа)",
                   command=self._suggest_threats_program_click).pack(side=tk.LEFT, padx=2)
        ttk.Button(sec_actions, text="🤖 Подобрать угрозы (ИИ)",
                   command=self._suggest_threats_ai_click).pack(side=tk.LEFT, padx=2)
        ttk.Button(sec_actions, text="⚖️ Сравнить",
                   command=self._compare_threats).pack(side=tk.LEFT, padx=2)
        ttk.Button(sec_actions, text="🤖 Проверить безопасность через ИИ",
                   command=self._check_security_ai).pack(side=tk.LEFT, padx=2)
        self._threats_date_label = ttk.Label(sec_actions, text="", foreground='gray')
        self._threats_date_label.pack(side=tk.LEFT, padx=8)

        # --- Две колонки: слева нарушитель+угрозы, справа остальное ---
        sec_cols = ttk.Frame(sec_frame)
        sec_cols.pack(fill=tk.BOTH, expand=True)
        sec_left = ttk.Frame(sec_cols)
        sec_left.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 6))
        sec_right = ttk.Frame(sec_cols)
        sec_right.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)

        # ===== Левая колонка =====
        ttk.Label(sec_left, text="Категория нарушителя:").pack(anchor=tk.W)
        self.is_attacker_category_combo = ttk.Combobox(sec_left, state='readonly')
        self.is_attacker_category_combo.pack(fill=tk.X, pady=(2, 4))
        try:
            import json as _json
            _ac = os.path.join(os.path.dirname(__file__), 'dictionaries', 'attacker_categories.json')
            with open(_ac, encoding='utf-8') as _f:
                self.is_attacker_category_combo['values'] = [''] + _json.load(_f)
        except Exception:
            pass

        ttk.Label(sec_left, text="Основные угрозы БИ:").pack(anchor=tk.W)
        sec_thr_row = ttk.Frame(sec_left)
        sec_thr_row.pack(fill=tk.BOTH, expand=True, pady=(2, 4))
        self.is_threats_listbox = tk.Listbox(sec_thr_row, height=7, exportselection=False)
        self.is_threats_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        sec_thr_btn = ttk.Frame(sec_thr_row)
        sec_thr_btn.pack(side=tk.RIGHT, padx=(4, 0))
        ttk.Button(sec_thr_btn, text="- Удалить",
                   command=lambda: self._del_sw_item(self.is_threats_listbox)).pack(fill=tk.X, pady=1)
        ttk.Button(sec_thr_btn, text="📚 Из словаря",
                   command=lambda: self._select_sw_from_dict(self.is_threats_listbox, 'threats_fstec.json')).pack(fill=tk.X, pady=1)

        # ===== Правая колонка =====
        def _sec_row(parent, label, attr, dict_name=None, height=2, allow_add=False):
            """Ряд: label + listbox + кнопки (+/-/✏️/📚)."""
            ttk.Label(parent, text=label).pack(anchor=tk.W)
            row = ttk.Frame(parent)
            row.pack(fill=tk.X, pady=(2, 4))
            lb = tk.Listbox(row, height=height, exportselection=False)
            lb.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
            setattr(self, attr, lb)
            btns = ttk.Frame(row)
            btns.pack(side=tk.RIGHT, padx=(4, 0))
            if allow_add or not dict_name:
                # ручное добавление: если разрешено или нет словаря
                ttk.Button(btns, text="+", width=2,
                           command=lambda l=lb: self._add_sw_item(l)).pack(pady=1)
            ttk.Button(btns, text="-", width=2,
                       command=lambda l=lb: self._del_sw_item(l)).pack(pady=1)
            ttk.Button(btns, text="✏️", width=3,
                       command=lambda l=lb: self._edit_sw_item(l)).pack(pady=1)
            if dict_name:
                ttk.Button(btns, text="📚", width=3,
                           command=lambda l=lb, d=dict_name: self._select_sw_from_dict(l, d)).pack(pady=1)

        _sec_row(sec_right, "Типы компьютерных инцидентов:",
                 'is_incidents_listbox', 'incidents.json', height=2)
        _sec_row(sec_right, "Показатели критериев значимости:",
                 'is_criteria_values_listbox', 'criteria_indicators.json', height=2)
        ttk.Button(sec_right, text="📋 Заполнить по таблице ПП № 127",
                   command=self._open_criteria_127_dialog).pack(anchor=tk.W, pady=(0, 4))
        _sec_row(sec_right, "Обоснование значений показателей:",
                 'is_criteria_justification_listbox', None, height=2)
        _sec_row(sec_right, "Организационные меры:",
                 'is_org_measures_listbox', 'org_measures.json', height=2, allow_add=True)
        _sec_row(sec_right, "Технические меры:",
                 'is_tech_measures_listbox', 'tech_measures.json', height=2, allow_add=True)

        # ===== Вкладка «Средства защиты» =====
        def_tab = ttk.Frame(nb2)
        nb2.add(def_tab, text="Средства защиты")
        def_btn_frame = ttk.Frame(def_tab)
        def_btn_frame.pack(fill=tk.X)
        ttk.Button(def_btn_frame, text="+ Добавить", command=self._add_defense_item).pack(side=tk.LEFT, padx=2)
        ttk.Button(def_btn_frame, text="- Удалить", command=lambda: self._del_from_listbox(self.defense_tools_listbox)).pack(side=tk.LEFT, padx=2)
        ttk.Button(def_btn_frame, text="Выбрать из списка", command=self._select_from_defense_list).pack(side=tk.LEFT, padx=2)
        self.defense_tools_listbox = tk.Listbox(def_tab, height=6)
        self.defense_tools_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)

        # ===== Вкладка «Пользователи ОКИИ» =====
        user_tab = ttk.Frame(nb2)
        nb2.add(user_tab, text="Пользователи ОКИИ")
        self.users_listbox = tk.Listbox(user_tab, height=6)
        self.users_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        btnf = ttk.Frame(user_tab)
        btnf.pack(pady=3)
        ttk.Button(btnf, text="👥 Выбрать из списка", command=self._select_users_from_employees).pack(side=tk.LEFT, padx=2)
        ttk.Button(btnf, text="+ Добавить", command=self._add_user_to_is).pack(side=tk.LEFT, padx=2)
        ttk.Button(btnf, text="- Удалить", command=self._del_user_from_is).pack(side=tk.LEFT, padx=2)

        self._set_is_edit_state(False)
        # Дата актуализации списка угроз ФСТЭК (если уже скачивался)
        self._update_threats_date_label()

    # ---------- ВКЛАДКА "КОМИССИЯ" ----------
    def _build_commission_tab(self):
        tab = ttk.Frame(self.notebook)
        self.notebook.add(tab, text="Комиссия")

        canvas = tk.Canvas(tab, highlightthickness=0)
        scrollbar = ttk.Scrollbar(tab, orient=tk.VERTICAL, command=canvas.yview)
        inner = ttk.Frame(canvas)
        inner.bind("<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
        canvas.create_window((0, 0), window=inner, anchor="nw")
        canvas.configure(yscrollcommand=scrollbar.set)
        canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)

        frame = ttk.Frame(inner, padding=10)
        frame.pack(fill=tk.X)

        ttk.Label(frame, text="Состав комиссии", font=('Arial', 11, 'bold')).pack(
            anchor=tk.W, pady=(0, 10))

        # Заголовки
        header = ttk.Frame(frame)
        header.pack(fill=tk.X)
        for w, col in [(20, "Роль"), (30, "Должность"), (25, "ФИО")]:
            ttk.Label(header, text=col, font=('', 9, 'bold'),
                      width=w, anchor=tk.W).pack(side=tk.LEFT, padx=2)

        self._comm_members_frame = ttk.Frame(frame)
        self._comm_members_frame.pack(fill=tk.X)

        ttk.Button(frame, text="+ Добавить члена комиссии",
                   command=self._add_commission_row_ui).pack(pady=10)

    # ---------- ВКЛАДКА "ПОЛЬЗОВАТЕЛИ" ----------
    def _build_users_tab(self):
        tab = ttk.Frame(self.notebook)
        self.notebook.add(tab, text="Пользователи")

        canvas = tk.Canvas(tab, highlightthickness=0)
        scrollbar = ttk.Scrollbar(tab, orient=tk.VERTICAL, command=canvas.yview)
        scrollable = ttk.Frame(canvas)
        scrollable.bind("<Configure>",
                        lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
        canvas.create_window((0, 0), window=scrollable, anchor="nw")
        canvas.configure(yscrollcommand=scrollbar.set)
        canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)

        frame = ttk.Frame(scrollable, padding=10)
        frame.pack(fill=tk.BOTH, expand=True)

        ttk.Label(frame, text="Сотрудники, имеющие доступ к объектам КИИ",
                  font=('Arial', 11, 'bold')).pack(anchor=tk.W, pady=(0, 10))

        # Заголовки
        header = ttk.Frame(frame)
        header.pack(fill=tk.X)
        for w, col in [(25, "Должность"), (30, "ФИО")]:
            ttk.Label(header, text=col, font=('', 9, 'bold'),
                      width=w, anchor=tk.W).pack(side=tk.LEFT, padx=2)

        self._employees_frame = ttk.Frame(frame)
        self._employees_frame.pack(fill=tk.X, pady=5)

        btn_frame = ttk.Frame(frame)
        btn_frame.pack(fill=tk.X, pady=5)
        ttk.Button(btn_frame, text="+ Добавить сотрудника",
                   command=self._add_employee_row_ui).pack(side=tk.LEFT, padx=2)
        ttk.Button(btn_frame, text="- Удалить выбранного",
                   command=self._delete_employee_selected).pack(side=tk.LEFT, padx=2)

        # Счётчик
        self._emp_count_label = ttk.Label(frame, text="Добавлено: 0")
        self._emp_count_label.pack(anchor=tk.W, pady=5)

        self._employee_entries = []
        # Загрузка существующих
        for emp in self.model.employees_access:
            self._add_employee_row(
                emp.get('position', ''),
                emp.get('fio', ''),
                
            )
        self._update_emp_count()

    def _add_employee_row(self, position="", fio=""):
        """Добавляет одну строку сотрудника."""
        row = ttk.Frame(self._employees_frame)
        row.pack(fill=tk.X, pady=2)

        pos_entry = ttk.Entry(row, width=25)
        pos_entry.insert(0, position)
        pos_entry.pack(side=tk.LEFT, padx=2)

        fio_entry = ttk.Entry(row, width=30)
        fio_entry.insert(0, fio)
        fio_entry.pack(side=tk.LEFT, padx=2)


        del_btn = ttk.Button(row, text="✖", width=3,
                             command=lambda: self._delete_employee_row(row))
        del_btn.pack(side=tk.LEFT, padx=5)

        self._employee_entries.append((row, pos_entry, fio_entry))
        self._update_emp_count()

    def _add_employee_row_ui(self):
        self._add_employee_row()

    def _delete_employee_row(self, row_frame):
        self._employee_entries = [e for e in self._employee_entries if e[0] != row_frame]
        row_frame.destroy()
        self._update_emp_count()

    def _delete_employee_selected(self):
        # Удаляем последнюю строку, если есть
        if self._employee_entries:
            row_frame = self._employee_entries[-1][0]
            self._delete_employee_row(row_frame)

    def _update_emp_count(self):
        count = len(self._employee_entries)
        if hasattr(self, '_emp_count_label'):
            self._emp_count_label.config(text=f"Добавлено: {count}")

    def _collect_employees(self):
        """Собирает данные сотрудников из GUI в модель."""
        self.model.employees_access.clear()
        for row, pos_e, fio_e in self._employee_entries:
            emp = {
                'position': pos_e.get().strip(),
                'fio': fio_e.get().strip(),
            }
            if emp['position'] or emp['fio']:
                self.model.employees_access.append(emp)

    # ---------- ВКЛАДКА "ГЕНЕРАЦИЯ" ----------
    def _build_generation_tab(self):
        tab = ttk.Frame(self.notebook)
        self.notebook.add(tab, text="Генерация")

        frame = ttk.Frame(tab, padding=10)
        frame.pack(fill=tk.BOTH, expand=True)

        ttk.Label(frame, text="Параметры генерации", font=('Arial', 11, 'bold')).pack(
            anchor=tk.W, pady=(0, 10))

        ttk.Label(frame, text="Папка шаблонов:").pack(anchor=tk.W)
        tf = ttk.Frame(frame)
        tf.pack(fill=tk.X, pady=(0, 5))
        ttk.Entry(tf, textvariable=self.templates_var).pack(side=tk.LEFT, fill=tk.X, expand=True)
        ttk.Button(tf, text="Обзор", command=self.select_templates).pack(side=tk.RIGHT, padx=5)

        ttk.Label(frame, text="Папка результатов:").pack(anchor=tk.W)
        sf = ttk.Frame(frame)
        sf.pack(fill=tk.X, pady=(0, 5))
        ttk.Entry(sf, textvariable=self.save_var).pack(side=tk.LEFT, fill=tk.X, expand=True)
        ttk.Button(sf, text="Обзор", command=self._select_save_dir).pack(side=tk.RIGHT, padx=5)

        ttk.Label(frame, text="Шаблоны для генерации:", font=('Arial', 10, 'bold')).pack(
            anchor=tk.W, pady=(10, 0))

        btn_frame = ttk.Frame(frame)
        btn_frame.pack(fill=tk.X, pady=(0, 5))
        ttk.Button(btn_frame, text="Выделить всё",
                   command=lambda: self._select_templates_all(True)).pack(side=tk.LEFT, padx=(0, 5))
        ttk.Button(btn_frame, text="Снять всё",
                   command=lambda: self._select_templates_all(False)).pack(side=tk.LEFT)

        self.template_listbox = tk.Listbox(frame, height=8, selectmode=tk.EXTENDED)
        self.template_listbox.pack(fill=tk.BOTH, expand=True, pady=5)
        self.template_listbox.bind('<<ListboxSelect>>', self._on_template_select)

        ttk.Button(frame, text="⚡ Сгенерировать выбранные документы",
                   command=self.generate_all).pack(fill=tk.X, pady=10)

    # ---------- ВКЛАДКА "ЛОГ" ----------
    def _build_log_tab(self):
        tab = ttk.Frame(self.notebook)
        self.notebook.add(tab, text="Лог")

        top = ttk.Frame(tab)
        top.pack(fill=tk.X, padx=5, pady=3)
        ttk.Button(top, text="💾 Экспорт логов…", command=self._export_logs).pack(side=tk.LEFT)
        ttk.Label(top, text="Журнал пишется в dokogen.log рядом с программой",
                  foreground='gray').pack(side=tk.LEFT, padx=8)

        self.log_text = scrolledtext.ScrolledText(tab, height=10)
        self.log_text.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
        self.log_text.insert(tk.END, "Лог готов\n")
        self.log_text.configure(state='disabled')

    # ==================== МЕТОДЫ РАБОТЫ С ДАННЫМИ ====================
    def _init_company(self):
        """Инициализация модели."""
        self.model = Company()
        # 3 члена комиссии по умолчанию
        for _ in range(3):
            self.model.commission.append(CommissionMember())

    def _collect_company(self):
        """Собирает данные из GUI в модель."""
        c = self.model
        c.full_name = self.company_name_entry.get().strip()
        c.short_name = self.short_name_entry.get().strip()
        for key, entry in self._company_entries.items():
            setattr(c, key, entry.get().strip())
        for key, entry in self._official_entries.items():
            setattr(c, key, entry.get().strip())
        c.contract_number = self.doc_number_var.get()
        c.contract_date = self.doc_date_var.get()
        if hasattr(self, 'okved_extra_listbox'):
            c.okved_extra = list(self.okved_extra_listbox.get(0, tk.END))

    def _collect_is(self):
        """Собирает данные объекта КИИ из GUI в модель."""
        if self._current_is_idx is not None and self._current_is_idx < len(self.model.information_systems):
            isys = self.model.information_systems[self._current_is_idx]
            isys.name = self.is_name_entry.get().strip()
            isys.description = self.is_description_entry.get().strip()
            if hasattr(self, 'is_software_entry'):
                isys.software = self.is_software_entry.get().strip()
            isys.category = self.is_category_var.get()
            if hasattr(self, 'is_balance_entry'):
                isys.balance_value = self.is_balance_entry.get().strip()
            # Поля 236 приказа
            for attr, model_field in [
                ('is_purpose_entry', 'purpose'),
                ('is_sphere_combo', 'sphere'),
                ('is_object_type_combo', 'object_type'),
                ('is_architecture_combo', 'architecture'),
                ('is_typical_object_combo', 'typical_object'),
                ('is_subject_name_entry', 'subject_name'),
                ('is_subject_address_entry', 'subject_address'),
                ('is_subject_element_entry', 'subject_element'),
                ('is_subject_inn_entry', 'subject_inn'),
                ('is_subject_kpp_entry', 'subject_kpp'),
                ('is_net_category_combo', 'net_category'),
                ('is_net_operator_entry', 'net_operator'),
                ('is_net_purpose_combo', 'net_purpose'),
                ('is_net_method_combo', 'net_method'),
                ('is_hw_sw_listbox', 'hw_sw'),
                ('is_os_sw_listbox', 'os_sw'),
                ('is_app_sw_listbox', 'app_sw'),
                ('is_attacker_category_combo', 'attacker_category'),
                ('is_threats_listbox', 'threats'),
                ('is_incidents_listbox', 'incidents'),
                ('is_criteria_values_listbox', 'criteria_values'),
                ('is_criteria_justification_listbox', 'criteria_justification'),
                ('is_org_measures_listbox', 'org_measures'),
                ('is_tech_measures_listbox', 'tech_measures'),
            ]:
                w = getattr(self, attr, None)
                if w is not None:
                    if isinstance(w, tk.Listbox):
                        setattr(isys, model_field, '\n'.join(w.get(0, tk.END)))
                    elif isinstance(w, tk.Text):
                        setattr(isys, model_field, w.get('1.0', 'end-1c').strip())
                    else:
                        setattr(isys, model_field, w.get().strip())
            # Списки
            isys.defense_tools_list = list(self.defense_tools_listbox.get(0, tk.END))
            isys.users_list = list(self.users_listbox.get(0, tk.END))

    def _collect_commission(self):
        """Собирает комиссию из GUI."""
        self.model.commission.clear()
        for row_frame, role_var, pos_entry, fio_entry in self.commission_entries:
            self.model.commission.append(
                CommissionMember(
                    role=role_var.get(),
                    position=pos_entry.get().strip(),
                    fio=fio_entry.get().strip(),
                )
            )

    def _company_to_gui(self):
        """Заполняет GUI из модели."""
        c = self.model
        self.company_name_entry.delete(0, tk.END)
        self.company_name_entry.insert(0, c.full_name or "")
        self.short_name_entry.delete(0, tk.END)
        self.short_name_entry.insert(0, c.short_name or "")

        for key, entry in self._company_entries.items():
            entry.delete(0, tk.END)
            entry.insert(0, getattr(c, key, "") or "")

        for key, entry in self._official_entries.items():
            entry.delete(0, tk.END)
            entry.insert(0, getattr(c, key, "") or "")

        self.doc_number_var.set(c.contract_number or "")
        self.doc_date_var.set(c.contract_date or "")

        # Дополнительные ОКВЭД
        if hasattr(self, 'okved_extra_listbox'):
            self.okved_extra_listbox.delete(0, tk.END)
            for item in (c.okved_extra or []):
                self.okved_extra_listbox.insert(tk.END, item)

        # Склонения
        idx_map = {'nomn': 1, 'gent': 2, 'datv': 3, 'accs': 4, 'ablt': 5, 'loct': 6}
        for code, entry in self.decl_entries.items():
            entry.delete(0, tk.END)
            entry.insert(0, getattr(c, f"company_name_{idx_map[code]}", "") or "")
        for code, entry in self.short_decl_entries.items():
            entry.delete(0, tk.END)
            entry.insert(0, getattr(c, f"short_name_{idx_map[code]}", "") or "")

        # Комиссия
        for frame in list(self._comm_members_frame.winfo_children()):
            frame.destroy()
        self.commission_entries.clear()
        for member in c.commission:
            self._add_commission_row(member.role, member.position, member.fio)
        if not self.commission_entries:
            for _ in range(3):
                self._add_commission_row()

        # ИС
        self._refresh_is_listbox()
        if self.model.information_systems:
            self.is_listbox.selection_set(0)
            self._on_is_select()

        # Пользователи
        if hasattr(self, '_employee_entries'):
            for row_frame, _, _ in self._employee_entries:
                row_frame.destroy()
            self._employee_entries.clear()
            for emp in c.employees_access:
                self._add_employee_row(
                    emp.get('position', ''),
                    emp.get('fio', ''),
                )
            self._update_emp_count()

        # Шаблоны
        self._on_templates_changed()

        # Автоопределение города из адреса после загрузки
        self._on_address_changed()

    # ==================== ГОРОД ИЗ АДРЕСА ====================
    def _extract_city_from_address(self, address: str) -> str:
        """Извлекает название города из адреса (простая эвристика)."""
        if not address:
            return ''
        # Пробуем найти "г. Город" или "Г Город" (без точки, заглавная)
        import re
        m = re.search(r'\b[гГ]\.?\s*([А-ЯЁ][а-яёA-Za-z-]+(?:\s+[А-ЯЁ][а-яёA-Za-z-]+)*)', address)
        if m:
            return m.group(1).strip()
        # Пробуем найти "город Город"
        m = re.search(r'\bгород\s+([А-ЯЁ][а-яёA-Za-z-]+(?:\s+[А-ЯЁ][а-яёA-Za-z-]+)*)', address, re.IGNORECASE)
        if m:
            return m.group(1).strip()
        # Пробуем первую часть до запятой, если там известный город
        parts = [p.strip() for p in address.split(',')]
        if parts:
            first = parts[0]
            # Если первая часть — короткая и не похожа на улицу/дом
            if len(first) <= 25 and first and not first.startswith(('ул', 'пр', 'пер', 'шоссе')):
                # Проверяем, что это похоже на город
                city_keywords = ['ск', 'цк', 'бург', 'град', 'полис', 'поль', 'стан', 'горск', 'чинск', 'янск', 'овск']
                if any(first.lower().endswith(kw) for kw in city_keywords):
                    return first
        return ''

    def _on_address_changed(self, event=None):
        """Обработчик изменения адреса — автозаполняет город."""
        addr_entry = self._company_entries.get('address')
        city_entry = self._company_entries.get('city_name')
        if not addr_entry or not city_entry:
            return
        address = addr_entry.get().strip()
        if not address:
            return
        current_city = city_entry.get().strip()
        if current_city:
            return  # Не перезаписываем, если уже заполнен
        city = self._extract_city_from_address(address)
        if city:
            city_entry.delete(0, tk.END)
            city_entry.insert(0, city)
            self.log(f"🏙 Город определён: {city}")

    # ==================== АВТОРАСЧЁТ УРОВНЯ ЗАЩИЩЁННОСТИ ====================
    # ==================== КОМИССИЯ ====================
    def _add_commission_row(self, role="Член комиссии", position="", fio=""):
        row = ttk.Frame(self._comm_members_frame)
        row.pack(fill=tk.X, pady=2)

        role_var = tk.StringVar(value=role)
        role_cb = ttk.Combobox(row, textvariable=role_var, state='readonly', width=22,
                               values=["Председатель комиссии", "Секретарь комиссии",
                                       "Член комиссии"])
        role_cb.pack(side=tk.LEFT, padx=2)

        pos_entry = ttk.Entry(row, width=30)
        pos_entry.insert(0, position)
        pos_entry.pack(side=tk.LEFT, padx=2)

        fio_entry = ttk.Entry(row, width=25)
        fio_entry.insert(0, fio)
        fio_entry.pack(side=tk.LEFT, padx=2)

        ttk.Button(row, text="✖", width=3,
                   command=lambda: self._delete_commission_row(row)).pack(side=tk.LEFT, padx=5)

        self.commission_entries.append((row, role_var, pos_entry, fio_entry))

    def _add_commission_row_ui(self):
        self._add_commission_row()

    def _delete_commission_row(self, row_frame):
        self.commission_entries = [e for e in self.commission_entries if e[0] != row_frame]
        row_frame.destroy()

    # ==================== ИНФОРМАЦИОННЫЕ СИСТЕМЫ ====================
    def _refresh_is_listbox(self):
        self.is_listbox.delete(0, tk.END)
        for isys in self.model.information_systems:
            self.is_listbox.insert(tk.END, isys.name or "(без названия)")
        if not self.model.information_systems:
            self._is_edit_panel.pack_forget()
            self._is_edit_panel.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5)
            self._set_is_edit_state(False)
        else:
            self._is_edit_panel.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5)

    def _set_is_edit_state(self, enabled):
        state = 'normal' if enabled else 'disabled'

    def _on_is_select(self, event=None):
        sel = self.is_listbox.curselection()
        if not sel:
            self._current_is_idx = None
            return

        # Сохраняем текущую ИС
        self._collect_is()

        idx = sel[0]
        if idx >= len(self.model.information_systems):
            return
        self._current_is_idx = idx
        isys = self.model.information_systems[idx]

        self.is_name_entry.delete(0, tk.END)
        self.is_name_entry.insert(0, isys.name or "")
        self.is_description_entry.delete(0, tk.END)
        self.is_description_entry.insert(0, isys.description or "")
        if hasattr(self, 'is_software_entry'):
            self.is_software_entry.delete(0, tk.END)
            self.is_software_entry.insert(0, isys.software or "")
        self.is_category_var.set(isys.category or "без категории")
        if hasattr(self, 'is_balance_entry'):
            self.is_balance_entry.delete(0, tk.END)
            self.is_balance_entry.insert(0, isys.balance_value or "")

        # Поля 236 приказа
        for attr, model_field in [
            ('is_purpose_entry', 'purpose'),
            ('is_sphere_combo', 'sphere'),
            ('is_object_type_combo', 'object_type'),
            ('is_architecture_combo', 'architecture'),
            ('is_typical_object_combo', 'typical_object'),
            ('is_subject_name_entry', 'subject_name'),
            ('is_subject_address_entry', 'subject_address'),
            ('is_subject_element_entry', 'subject_element'),
            ('is_subject_inn_entry', 'subject_inn'),
            ('is_subject_kpp_entry', 'subject_kpp'),
            ('is_net_category_combo', 'net_category'),
            ('is_net_operator_entry', 'net_operator'),
            ('is_net_purpose_combo', 'net_purpose'),
            ('is_net_method_combo', 'net_method'),
            ('is_hw_sw_listbox', 'hw_sw'),
            ('is_os_sw_listbox', 'os_sw'),
            ('is_app_sw_listbox', 'app_sw'),
            ('is_attacker_category_combo', 'attacker_category'),
            ('is_threats_listbox', 'threats'),
            ('is_incidents_listbox', 'incidents'),
            ('is_criteria_values_listbox', 'criteria_values'),
            ('is_criteria_justification_listbox', 'criteria_justification'),
            ('is_org_measures_listbox', 'org_measures'),
            ('is_tech_measures_listbox', 'tech_measures'),
        ]:
            w = getattr(self, attr, None)
            if w is not None:
                val = getattr(isys, model_field, '') or ''
                if isinstance(w, ttk.Combobox):
                    w.set(val)
                elif isinstance(w, tk.Listbox):
                    w.delete(0, tk.END)
                    for line in str(val).split('\n'):
                        if line.strip():
                            w.insert(tk.END, line.strip())
                elif isinstance(w, tk.Text):
                    w.delete('1.0', tk.END)
                    w.insert('1.0', val)
                else:
                    w.delete(0, tk.END)
                    w.insert(0, val)

        # Списки
        for lb, items in [
            (self.defense_tools_listbox, isys.defense_tools_list or []),
            (self.users_listbox, isys.users_list or []),
        ]:
            lb.delete(0, tk.END)
            for item in items:
                lb.insert(tk.END, item)

    def _add_is(self):
        # Сохраняем текущую ИС перед добавлением новой
        self._collect_is()
        name = simpledialog.askstring("Добавить ИС", "Введите наименование ИС:")
        if name:
            isys = InformationSystem(name=name)
            self.model.information_systems.append(isys)
            self._refresh_is_listbox()
            self.is_listbox.selection_set(tk.END)
            self._on_is_select()

    def _copy_is(self):
        if self._current_is_idx is None:
            return
        self._collect_is()
        src = self.model.information_systems[self._current_is_idx]
        copy = InformationSystem(
            name=src.name + " (копия)",
            description=src.description,
            is_local_network=src.is_local_network,
            is_internet=src.is_internet,
            category=src.category,
            defense_tools_list=list(src.defense_tools_list),
            users_list=list(src.users_list),
            purpose=src.purpose,
            sphere=src.sphere,
            object_type=src.object_type,
            architecture=src.architecture,
            typical_object=src.typical_object,
            subject_name=src.subject_name,
            subject_address=src.subject_address,
            subject_element=src.subject_element,
            subject_inn=src.subject_inn,
            subject_kpp=src.subject_kpp,
            net_category=src.net_category,
            net_operator=src.net_operator,
            net_purpose=src.net_purpose,
            net_method=src.net_method,
            hw_sw=src.hw_sw,
            os_sw=src.os_sw,
            app_sw=src.app_sw,
            balance_value=src.balance_value,
            attacker_category=src.attacker_category,
            threats=src.threats,
            incidents=src.incidents,
            criteria_values=src.criteria_values,
            criteria_justification=src.criteria_justification,
            org_measures=src.org_measures,
            tech_measures=src.tech_measures,
        )
        self.model.information_systems.append(copy)
        self._refresh_is_listbox()
        self.is_listbox.selection_set(tk.END)
        self._on_is_select()

    def _delete_is(self):
        if self._current_is_idx is None:
            return
        self._collect_is()
        del self.model.information_systems[self._current_is_idx]
        self._current_is_idx = None
        self._refresh_is_listbox()
        if self.model.information_systems:
            self.is_listbox.selection_set(0)
            self._on_is_select()

    def _add_defense_item(self):
        item = simpledialog.askstring("Добавить СЗИ", "Введите средство защиты:")
        if item:
            self.defense_tools_listbox.insert(tk.END, item)

    def _select_sw_from_dict(self, listbox, dict_name, allow_refine=False):
        """Выбор позиций из словаря (JSON) — можно несколько.

        allow_refine=True — показывать кнопку «✏️ Уточнить модель/кол-во»
        (только для ПО/оборудования, где это имеет смысл).
        """
        import json
        dict_path = os.path.join(os.path.dirname(__file__), 'dictionaries', dict_name)
        try:
            with open(dict_path, 'r', encoding='utf-8') as f:
                items = json.load(f)
            title = "Выбор из словаря"
            self._select_from_dialog(title, items, listbox, allow_refine=allow_refine)
        except Exception as e:
            messagebox.showerror("Ошибка", f"Не удалось загрузить словарь: {e}")

    def _sw_dialog(self, title, initial=""):
        """Большое окно для ввода/редактирования позиции ПО."""
        dialog = tk.Toplevel(self.root)
        dialog.title(title)
        dialog.geometry("560x220")
        dialog.transient(self.root)
        dialog.grab_set()
        dialog.resizable(True, True)

        frame = ttk.Frame(dialog, padding=10)
        frame.pack(fill=tk.BOTH, expand=True)

        ttk.Label(frame, text="Текст позиции:").pack(anchor=tk.W)
        txt = tk.Text(frame, width=70, height=6, wrap=tk.WORD)
        txt.pack(fill=tk.BOTH, expand=True, pady=5)
        txt.insert('1.0', initial)
        txt.focus_set()

        result = {}

        def _ok():
            result['val'] = txt.get('1.0', 'end-1c').strip()
            dialog.destroy()

        def _cancel():
            result['val'] = None
            dialog.destroy()

        btn_frame = ttk.Frame(frame)
        btn_frame.pack(fill=tk.X)
        ttk.Button(btn_frame, text="✅ ОК", command=_ok).pack(side=tk.RIGHT, padx=2)
        ttk.Button(btn_frame, text="Отмена", command=_cancel).pack(side=tk.RIGHT, padx=2)

        dialog.bind('<Return>', lambda e: _ok())
        dialog.bind('<Escape>', lambda e: _cancel())
        dialog.wait_window()
        return result.get('val')

    def _add_sw_item(self, listbox):
        """Добавить строку в список ПО (большое окно)."""
        item = self._sw_dialog("Добавить позицию")
        if item:
            listbox.insert(tk.END, item)

    def _del_sw_item(self, listbox):
        """Удалить выбранную строку из списка ПО."""
        sel = listbox.curselection()
        if sel:
            listbox.delete(sel[0])

    def _edit_sw_item(self, listbox):
        """Редактировать выбранную строку списка ПО (большое окно)."""
        sel = listbox.curselection()
        if not sel:
            messagebox.showinfo("Редактировать", "Сначала выберите позицию в списке")
            return
        current = listbox.get(sel[0])
        item = self._sw_dialog("Редактировать позицию", initial=current)
        if item is not None:
            listbox.delete(sel[0])
            listbox.insert(sel[0], item)

    def _add_user_to_is(self):
        user = simpledialog.askstring("Добавить пользователя", "Введите ФИО или должность:")
        if user:
            self.users_listbox.insert(tk.END, user)

    def _open_criteria_127_dialog(self):
        """Окно заполнения показателей критериев значимости по ПП РФ № 127.

        Таблица перенесена дословно из Постановления Правительства РФ от 08.02.2019 № 127:
        5 критериев (социальная, политическая, экономическая, экологическая,
        значимость для обороны страны), показатели 1-14 с подпунктами,
        значения сразу привязаны к категориям III / II / I.

        Категория значимости объекта определяется по наибольшей категории,
        которой соответствует хотя бы один выбранный показатель:
        I → 1-я категория, II → 2-я, III → 3-я, иначе «без категории».
        """
        import json as _json
        dict_path = os.path.join(os.path.dirname(__file__), 'dictionaries', 'criteria_127.json')
        try:
            with open(dict_path, encoding='utf-8') as f:
                criteria = _json.load(f)
        except Exception as e:
            messagebox.showerror("Ошибка", f"Не удалось загрузить таблицу ПП 127: {e}")
            return

        dialog = tk.Toplevel(self.root)
        dialog.title("Показатели критериев значимости (ПП РФ № 127)")
        dialog.geometry("980x700")
        dialog.transient(self.root)
        dialog.grab_set()

        outer = ttk.Frame(dialog, padding=8)
        outer.pack(fill=tk.BOTH, expand=True)

        canvas = tk.Canvas(outer, highlightthickness=0)
        scrollbar = ttk.Scrollbar(outer, orient=tk.VERTICAL, command=canvas.yview)
        body = ttk.Frame(canvas)
        body.bind("<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
        canvas.create_window((0, 0), window=body, anchor="nw")
        canvas.configure(yscrollcommand=scrollbar.set)
        canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)

        ttk.Label(body, text="Показатели критериев значимости объекта КИИ",
                  font=('Arial', 11, 'bold')).pack(anchor=tk.W, pady=(0, 2))
        ttk.Label(body, text="Постановление Правительства РФ от 08.02.2019 № 127 «Об утверждении Правил категорирования "
                             "объектов критической информационной инфраструктуры РФ» (приложение). "
                             "По каждому показателю выберите значение, которому соответствует объект. "
                             "Категория определяется по наивысшей из выбранных.",
                  foreground='gray', wraplength=920, justify=tk.LEFT).pack(anchor=tk.W, pady=(0, 8))

        rows = []  # (num, name, values{III:..,II:..,I:..}, var)

        for crit in criteria:
            grp = ttk.LabelFrame(body, text=crit.get('criteria', ''), padding=6)
            grp.pack(fill=tk.X, pady=4)
            for ind in crit.get('indicators', []):
                num = ind.get('num', '')
                name = ind.get('name', '')
                values = ind.get('values', {})
                row = ttk.Frame(grp)
                row.pack(fill=tk.X, pady=2)
                # Номер показателя
                ttk.Label(row, text=num, width=4, font=('Arial', 9, 'bold')).pack(side=tk.LEFT)
                # Текст показателя
                ttk.Label(row, text=name, wraplength=640, justify=tk.LEFT).pack(side=tk.LEFT)
                # Выбор значения (категория)
                var = tk.StringVar(value='')
                options = ['']
                for cat in ('III', 'II', 'I'):
                    v = values.get(cat, '')
                    if v:
                        options.append(f"{cat} категория: {v}")
                combo = ttk.Combobox(row, textvariable=var, state='readonly', width=34,
                                     values=options)
                combo.pack(side=tk.RIGHT, padx=6)
                rows.append((num, name, values, var))

        # Обоснование
        ttk.Label(body, text="Обоснование полученных значений:", font=('Arial', 9, 'bold')).pack(
            anchor=tk.W, pady=(10, 2))
        justify_txt = tk.Text(body, width=110, height=5, wrap=tk.WORD)
        justify_txt.pack(fill=tk.X, pady=(0, 6))
        if hasattr(self, 'is_criteria_justification_listbox') and self.is_criteria_justification_listbox.size():
            justify_txt.insert('1.0', '\n'.join(
                self.is_criteria_justification_listbox.get(i)
                for i in range(self.is_criteria_justification_listbox.size())))

        result_lbl = ttk.Label(body, text="Категория значимости: —", font=('Arial', 10, 'bold'))
        result_lbl.pack(anchor=tk.W, pady=(6, 0))

        def _selected():
            """Список выбранных: [(num, name, категория, значение)]."""
            out = []
            for num, name, values, var in rows:
                s = var.get().strip()
                if not s:
                    continue
                cat = s.split(' ')[0]  # III/II/I
                val = s.split(': ', 1)[1] if ': ' in s else ''
                out.append((num, name, cat, val))
            return out

        def _calc_category(selected):
            """Категория по наивысшей из выбранных."""
            cats = [c for _, _, c, _ in selected]
            if 'I' in cats:
                return '1'
            if 'II' in cats:
                return '2'
            if 'III' in cats:
                return '3'
            return 'без категории'

        def _calc():
            selected = _selected()
            if not selected:
                result_lbl.config(text="Категория значимости: — (ничего не выбрано)")
                return
            cat = _calc_category(selected)
            result_lbl.config(text=f"Категория значимости: {cat} (выбрано показателей: {len(selected)})")

        def _save():
            selected = _selected()
            # Заполняем список показателей
            lines = []
            for num, name, cat, val in selected:
                lines.append(f"Показатель {num}. {name} — {cat} категория: {val}")
            if hasattr(self, 'is_criteria_values_listbox'):
                self.is_criteria_values_listbox.delete(0, tk.END)
                for line in lines:
                    self.is_criteria_values_listbox.insert(tk.END, line)
            # Категория значимости
            cat = _calc_category(selected)
            if hasattr(self, 'is_category_var'):
                self.is_category_var.set(cat)
            # Обоснование: если поле пустое — сформируем автоматически
            j = justify_txt.get('1.0', 'end-1c').strip()
            auto_just = (
                f"Категория значимости объекта КИИ определена в соответствии с Правилами категорирования "
                f"объектов критической информационной инфраструктуры РФ (ПП РФ от 08.02.2019 № 127). "
                f"Объект отнесён к {cat} категории значимости. Основания: "
                + "; ".join(f"показатель {num} ({cat} категория)" for num, _, cat, _ in selected) + "."
            )
            if not j:
                j = auto_just
            if hasattr(self, 'is_criteria_justification_listbox'):
                self.is_criteria_justification_listbox.delete(0, tk.END)
                for line in j.split('\n'):
                    if line.strip():
                        self.is_criteria_justification_listbox.insert(tk.END, line.strip())
            self.log(f"📋 Критерии значимости (ПП 127) сохранены: категория {cat}, показателей: {len(selected)}")
            dialog.destroy()

        btn_frame = ttk.Frame(body)
        btn_frame.pack(fill=tk.X, pady=(8, 0))
        ttk.Button(btn_frame, text="🧮 Рассчитать категорию", command=_calc).pack(side=tk.LEFT, padx=2)
        ttk.Button(btn_frame, text="💾 Сохранить", command=_save).pack(side=tk.LEFT, padx=2)
        ttk.Button(btn_frame, text="Отмена", command=dialog.destroy).pack(side=tk.LEFT, padx=2)

        dialog.wait_window()

    def _download_fstec_threats(self):
        """Скачать перечень угроз с сайта ФСТЭК (БДУ) и обновить список."""
        try:
            from .fstec_threats import download_fstec_threats, get_threats_date
        except ImportError as e:
            messagebox.showerror("Ошибка", f"Модуль fstec_threats не найден: {e}")
            return

        self.log("⬇️ Загрузка перечня угроз с bdu.fstec.ru…")
        ok, msg, threats = download_fstec_threats()
        if not ok:
            messagebox.showerror("Загрузка угроз",
                                 f"Не удалось загрузить:\n{msg}\n\n"
                                 "Проверьте доступ к интернету или попробуйте позже.")
            self.log(f"❌ Загрузка угроз: {msg}")
            return

        # Обновляем label даты актуализации
        self._update_threats_date_label()
        # Если список угроз уже открыт — можно показать диалог выбора
        messagebox.showinfo("Загрузка угроз",
                            f"{msg}\n\nСписок сохранён. Теперь нажмите «📚 Из словаря» "
                            "в разделе «Основные угрозы БИ», чтобы выбрать нужные угрозы.")
        self.log(f"✅ {msg}")

    def _update_threats_date_label(self):
        """Обновить подпись даты актуализации списка угроз."""
        try:
            from .fstec_threats import get_threats_date
            d = get_threats_date()
        except Exception:
            d = None
        if hasattr(self, '_threats_date_label'):
            if d:
                self._threats_date_label.config(
                    text=f"Актуально на: {d}", foreground='#2e7d32')
            else:
                self._threats_date_label.config(
                    text="⚠ Список угроз ещё не скачан — нажмите «⬇️ Загрузить угрозы ФСТЭК»",
                    foreground='#c62828')

    def _suggest_threats_program_click(self):
        """Подобрать угрозы логикой программы по характеристикам объекта."""
        if self._current_is_idx is None:
            messagebox.showinfo("Угрозы", "Сначала выберите объект КИИ в списке слева")
            return
        self._collect_is()
        isys = self.model.information_systems[self._current_is_idx]
        try:
            from .threat_rules import suggest_threats_program
            threats = suggest_threats_program(isys)
        except Exception as e:
            messagebox.showerror("Ошибка", str(e))
            return
        if not threats:
            messagebox.showinfo("Угрозы", "Не удалось подобрать угрозы.\n"
                                         "Скачайте перечень ФСТЭК: кнопка «⬇️ Загрузить угрозы ФСТЭК».")
            return
        lb = self.is_threats_listbox
        lb.delete(0, tk.END)
        for t in threats:
            lb.insert(tk.END, t)
        self._collect_is()  # сохранить в модель — галочки в словаре будут стоять
        self.log(f"⚙️ Угрозы подобраны программой: {len(threats)} шт.")

    def _suggest_threats_ai_click(self):
        """Подобрать угрозы через ИИ по характеристикам объекта."""
        if self._current_is_idx is None:
            messagebox.showinfo("Угрозы", "Сначала выберите объект КИИ в списке слева")
            return
        settings = load_api_settings()
        if not settings.get('ai_api_key'):
            messagebox.showwarning("Настройки", "Не задан API-ключ ИИ.\nМеню «Файл → Настройки»")
            return
        self._collect_is()
        isys = self.model.information_systems[self._current_is_idx]
        from .threat_rules import _has_internet, load_threats
        has_internet = _has_internet(isys)
        data = {
            'name': isys.name or '',
            'description': isys.description or '',
            'object_type': isys.object_type or '',
            'sphere': isys.sphere or '',
            'architecture': isys.architecture or '',
            'net_category': isys.net_category or '',
            'attacker_category': isys.attacker_category or '',
            'has_internet': has_internet,
            'summary': (
                f"Объект КИИ: {isys.name or '—'}. "
                f"Тип: {isys.object_type or '—'}. "
                f"Сфера: {isys.sphere or '—'}. "
                f"Выход в интернет: {'есть' if has_internet else 'нет'}. "
                f"Категория нарушителя: {isys.attacker_category or '—'}. "
                f"Сети: {isys.net_category or '—'}"
            ),
        }
        threats_full = load_threats()  # свежий перечень с сайта ФСТЭК
        self.log(f"🤖 Запрос к ИИ: подбор угроз для «{isys.name}» (перечень: {len(threats_full)} угроз)")
        self._verify_log("Подбор угроз через ИИ…")

        def _run():
            try:
                from .verify import suggest_threats_ai
                threats, err = suggest_threats_ai(
                    data, settings['ai_api_key'], settings.get('ai_model'),
                    settings.get('ai_base_url'), threats=threats_full)
                self.root.after(0, lambda: self._suggest_threats_ai_done(threats, err))
            except Exception as e:
                self.root.after(0, lambda: self._suggest_threats_ai_done(None, str(e)))

        threading.Thread(target=_run, daemon=True).start()

    def _suggest_threats_ai_done(self, threats, err):
        if err:
            self._verify_log(f"❌ {err}")
            self.log(f"❌ Подбор угроз ИИ: {err}")
            return
        if not threats:
            self._verify_log("❌ ИИ не вернул угрозы")
            return
        lb = self.is_threats_listbox
        lb.delete(0, tk.END)
        for t in threats:
            lb.insert(tk.END, t)
        self._collect_is()  # сохранить в модель — галочки в словаре будут стоять
        self.log(f"🤖 ИИ подобрал угроз: {len(threats)}")
        self._verify_log(f"🤖 ИИ: {len(threats)} угроз")

    def _compare_threats(self):
        """Сравнить угрозы, подобранные программой и ИИ."""
        if self._current_is_idx is None:
            messagebox.showinfo("Сравнение", "Сначала выберите объект КИИ в списке слева")
            return
        self._collect_is()
        isys = self.model.information_systems[self._current_is_idx]
        from .threat_rules import suggest_threats_program
        program = suggest_threats_program(isys)
        current = [str(self.is_threats_listbox.get(i)) for i in range(self.is_threats_listbox.size())]
        if not current:
            messagebox.showinfo("Сравнение", "Сначала подберите угрозы (программой или ИИ).")
            return

        # Показываем окно: слева подобрано программой, справа текущий список
        dlg = tk.Toplevel(self.root)
        dlg.title("Сравнение угроз: программа ↔ ИИ/текущий")
        dlg.geometry("760x420")
        dlg.transient(self.root)

        frm = ttk.Frame(dlg, padding=8)
        frm.pack(fill=tk.BOTH, expand=True)

        left = ttk.Frame(frm)
        left.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        ttk.Label(left, text="⚙️ Программа", font=('Arial', 9, 'bold')).pack(anchor=tk.W)
        lb_p = tk.Listbox(left, height=14)
        lb_p.pack(fill=tk.BOTH, expand=True, pady=4)
        for t in program:
            lb_p.insert(tk.END, t)

        right = ttk.Frame(frm)
        right.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(8, 0))
        ttk.Label(right, text="🤖 ИИ / текущий список", font=('Arial', 9, 'bold')).pack(anchor=tk.W)
        lb_c = tk.Listbox(right, height=14)
        lb_c.pack(fill=tk.BOTH, expand=True, pady=4)
        for t in current:
            lb_c.insert(tk.END, t)

        # Подсветка совпадений
        pset = set(program)
        for i in range(lb_p.size()):
            if lb_p.get(i) in set(current):
                lb_p.itemconfig(i, bg='#c8e6c9')
        for i in range(lb_c.size()):
            if lb_c.get(i) in pset:
                lb_c.itemconfig(i, bg='#c8e6c9')

        info = ttk.Label(frm, text="Зелёным выделены совпавшие угрозы.",
                         foreground='gray')
        info.pack(anchor=tk.W, pady=(4, 0))

    def _check_security_ai(self):
        """Проверка вкладки «Безопасность» через ИИ (DeepSeek/OpenAI-совместимый API)."""
        if self._current_is_idx is None:
            messagebox.showinfo("Безопасность", "Сначала выберите объект КИИ в списке слева")
            return
        settings = load_api_settings()
        if not settings.get('ai_api_key'):
            messagebox.showwarning("Настройки", "Не задан API-ключ ИИ.\nМеню «Файл → Настройки»")
            return

        # Собираем текущие данные безопасности объекта
        self._collect_is()
        isys = self.model.information_systems[self._current_is_idx]
        security_data = {
            'name': isys.name or '',
            'description': isys.description or '',
            'category': isys.category or '',
            'sphere': isys.sphere or '',
            'object_type': isys.object_type or '',
            'architecture': isys.architecture or '',
            'attacker_category': isys.attacker_category or '',
            'threats': isys.threats or '',
            'incidents': isys.incidents or '',
            'criteria_values': isys.criteria_values or '',
            'criteria_justification': isys.criteria_justification or '',
            'org_measures': isys.org_measures or '',
            'tech_measures': isys.tech_measures or '',
        }
        self.log(f"🤖 Запрос к ИИ ({settings.get('ai_model') or 'deepseek-chat'}): проверка безопасности")
        self._verify_log("Проверка безопасности через ИИ…")

        def _run():
            try:
                from .verify import check_security_ai
                result, err = check_security_ai(
                    security_data,
                    settings['ai_api_key'],
                    settings.get('ai_model'),
                    settings.get('ai_base_url'),
                )
                self.root.after(0, lambda: self._security_ai_done(result, err))
            except Exception as e:
                self.root.after(0, lambda: self._security_ai_done(None, str(e)))

        threading.Thread(target=_run, daemon=True).start()

    def _security_ai_done(self, result, err):
        """Обработка ответа ИИ по безопасности."""
        if err:
            self._verify_log(f"❌ {err}")
            self.log(f"❌ Проверка безопасности: {err}")
            return
        d = result['data']
        lines = [f"Источник: {result['source']}"]
        lines.append(f"Оценка: {d.get('rating', 'warning')}")
        notes = d.get('notes') or []
        if notes:
            lines.append("\nЗамечания:")
            for n in notes:
                lines.append(f"  • {n}")
        props = d.get('proposals') or {}
        if any(props.values()):
            lines.append("\nПредложения ИИ:")
            for key, label in [('attacker_category', 'Нарушитель'),
                               ('threats', 'Угрозы'),
                               ('incidents', 'Инциденты'),
                               ('criteria_values', 'Показатели'),
                               ('criteria_justification', 'Обоснование'),
                               ('org_measures', 'Орг. меры'),
                               ('tech_measures', 'Тех. меры')]:
                v = props.get(key)
                if v:
                    lines.append(f"  {label}: {v}")
        self._verify_log('\n'.join(lines))
        self.log(f"🤖 ИИ: оценка {d.get('rating', 'warning')}, замечаний: {len(notes)}")

    def _fill_subject_from_company(self):
        """Заполнить поля субъекта данными организации (если объект в той же организации)."""
        if self._current_is_idx is None:
            messagebox.showinfo("Субъект", "Сначала выберите объект КИИ в списке слева")
            return
        c = self.model
        if not (c.full_name or c.inn):
            messagebox.showinfo("Субъект",
                                "Вкладка «Организация» пуста.\nСначала заполните данные организации "
                                "(или импортируйте опросник).")
            return

        def _set_entry(attr, value):
            w = getattr(self, attr, None)
            if w is not None:
                w.delete(0, tk.END)
                w.insert(0, value or '')

        _set_entry('is_subject_name_entry', c.full_name or c.short_name)
        _set_entry('is_subject_address_entry', c.address)
        _set_entry('is_subject_inn_entry', c.inn)
        _set_entry('is_subject_kpp_entry', c.kpp)
        # Элемент (компонент) объекта — оставляем как есть, заполняется вручную
        self.log("📋 Субъект заполнен данными организации")
        messagebox.showinfo("Субъект",
                            "Поля «Наименование», «Адрес», «ИНН», «КПП» заполнены из вкладки «Организация».\n"
                            "«Элемент (компонент) объекта» заполните вручную.")

    def _select_users_from_employees(self):
        """Выбор пользователей ИС из списка сотрудников организации (вкладка «Пользователи»)."""
        names = []
        for emp in self.model.employees_access:
            fio = (emp.get('fio') or '').strip()
            if fio and fio not in names:
                names.append(fio)
        # Дополнительно: пользователи из других ИС
        for isys in self.model.information_systems:
            for u in (isys.users_list or []):
                u = u.strip()
                if u and u not in names:
                    names.append(u)
        if not names:
            messagebox.showinfo(
                "Выбор пользователей",
                "Список пуст.\nЗаполните вкладку «Пользователи» (сотрудники с доступом к объектам КИИ) "
                "или добавьте пользователей в другой ИС."
            )
            return
        self._select_from_dialog("Выбор пользователей ИС", names, self.users_listbox)

    def _del_user_from_is(self):
        sel = self.users_listbox.curselection()
        if sel:
            self.users_listbox.delete(sel[0])

    def _del_from_listbox(self, listbox):
        """Удаление выбранного элемента из любого listbox."""
        sel = listbox.curselection()
        if sel:
            listbox.delete(sel[0])

    # ==================== ВЫБОР ИЗ СПИСКА (словари) ====================
    def _select_from_dialog(self, title: str, items: list, listbox, allow_refine=False):
        """Открывает окно с чекбоксами для выбора элементов из списка.

        allow_refine=True — показывать кнопку «✏️ Уточнить модель/кол-во».
        """
        dialog = tk.Toplevel(self.root)
        dialog.title(title)
        dialog.geometry("720x680")
        dialog.transient(self.root)
        dialog.grab_set()

        frame = ttk.Frame(dialog, padding=10)
        frame.pack(fill=tk.BOTH, expand=True)

        ttk.Label(frame, text=title, font=('Arial', 10, 'bold')).pack(anchor=tk.W, pady=(0, 5))

        # Поиск (важно для большого перечня угроз ФСТЭК)
        search_var = tk.StringVar()
        search_entry = ttk.Entry(frame, textvariable=search_var)
        search_entry.pack(fill=tk.X, pady=(0, 5))
        search_entry.insert(0, "")
        search_entry.focus_set()

        # Уже выбранные элементы
        existing = set(listbox.get(0, tk.END))

        canvas = tk.Canvas(frame, highlightthickness=0)
        scrollbar = ttk.Scrollbar(frame, orient=tk.VERTICAL, command=canvas.yview)
        check_frame = ttk.Frame(canvas)
        check_frame.bind("<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
        canvas.create_window((0, 0), window=check_frame, anchor="nw")
        canvas.configure(yscrollcommand=scrollbar.set)
        canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)

        vars = {}
        items_map = {}
        for item in items:
            var = tk.BooleanVar(value=item in existing)
            cb = ttk.Checkbutton(check_frame, text=item, variable=var)
            cb.pack(anchor=tk.W, pady=1)
            vars[item] = var
            items_map[item] = (var, cb)

        # Фильтр по поиску: скрываем неподходящие чекбоксы
        def _apply_search(*_):
            q = search_var.get().strip().lower()
            for item, (var, cb) in items_map.items():
                if q and q not in item.lower():
                    cb.pack_forget()
                else:
                    cb.pack(anchor=tk.W, pady=1)

        search_var.trace_add('write', _apply_search)

        btn_frame = ttk.Frame(dialog, padding=10)
        btn_frame.pack(fill=tk.X)

        def _apply():
            selected = [item for item, var in vars.items() if var.get()]
            listbox.delete(0, tk.END)
            for item in selected:
                listbox.insert(tk.END, item)
            dialog.destroy()

        def _select_all():
            for var in vars.values():
                var.set(True)

        def _clear_all():
            for var in vars.values():
                var.set(False)

        def _refine_selected():
            """Для отмеченных позиций запросить модель/версию и количество."""
            for item, (var, cb) in list(items_map.items()):
                if not var.get():
                    continue
                new_text = self._refine_dict_item_dialog(item)
                if new_text is None or new_text == item:
                    continue
                # Обновляем текст чекбокса и ключ в словаре
                cb.config(text=new_text)
                vars.pop(item, None)
                items_map.pop(item, None)
                vars[new_text] = var
                items_map[new_text] = (var, cb)

        if allow_refine:
            ttk.Button(btn_frame, text="✏️ Уточнить модель/кол-во",
                       command=_refine_selected).pack(side=tk.LEFT, padx=2)
        ttk.Button(btn_frame, text="Выбрать всё", command=_select_all).pack(side=tk.LEFT, padx=2)
        ttk.Button(btn_frame, text="Снять всё", command=_clear_all).pack(side=tk.LEFT, padx=2)
        ttk.Button(btn_frame, text="✅ Применить", command=_apply).pack(side=tk.RIGHT, padx=2)

        dialog.wait_window()

    def _refine_dict_item_dialog(self, base_item):
        """Окно уточнения модели и количества для позиции из словаря.

        Возвращает полную строку вида «Windows 10 Pro 5 шт.» или
        «Сервер Supermicro SSG-2027R -E1R24N 2 шт.» или None.
        """
        import re
        # Убираем из базы маркер «(уточнить …)»
        base = re.sub(r'\s*\([^)]*уточн[^)]*\)', '', base_item, flags=re.I).strip() or base_item

        dialog = tk.Toplevel(self.root)
        dialog.title("Уточнить модель и количество")
        dialog.geometry("500x180")
        dialog.transient(self.root)
        dialog.grab_set()

        frame = ttk.Frame(dialog, padding=10)
        frame.pack(fill=tk.BOTH, expand=True)

        ttk.Label(frame, text=f"Позиция: {base}", wraplength=470,
                  font=('Arial', 9, 'bold')).pack(anchor=tk.W, pady=(0, 8))

        ttk.Label(frame, text="Модель / версия / конфигурация (необязательно):").pack(anchor=tk.W)
        model_entry = ttk.Entry(frame, width=60)
        model_entry.pack(fill=tk.X, pady=(2, 6))
        model_entry.focus_set()

        ttk.Label(frame, text="Количество:").pack(anchor=tk.W)
        count_entry = ttk.Entry(frame, width=20)
        count_entry.pack(anchor=tk.W, pady=(2, 6))

        result = {}

        def _ok():
            model = model_entry.get().strip()
            count = count_entry.get().strip()
            text = base
            if model:
                text = f"{text} {model}"
            if count:
                text = f"{text} {count} шт."
            result['val'] = text
            dialog.destroy()

        def _cancel():
            result['val'] = None
            dialog.destroy()

        btn_frame = ttk.Frame(frame)
        btn_frame.pack(fill=tk.X, pady=(6, 0))
        ttk.Button(btn_frame, text="✅ ОК", command=_ok).pack(side=tk.RIGHT, padx=2)
        ttk.Button(btn_frame, text="Отмена", command=_cancel).pack(side=tk.RIGHT, padx=2)

        dialog.bind('<Return>', lambda e: _ok())
        dialog.bind('<Escape>', lambda e: _cancel())
        dialog.wait_window()
        return result.get('val')

    def _select_from_defense_list(self):
        """Выбор СЗИ из JSON словаря."""
        import json
        dict_path = os.path.join(os.path.dirname(__file__), 'dictionaries', 'defense_tools.json')
        try:
            with open(dict_path, 'r', encoding='utf-8') as f:
                items = json.load(f)
            self._select_from_dialog("Выбор средств защиты информации", items, self.defense_tools_listbox)
        except Exception as e:
            messagebox.showerror("Ошибка", f"Не удалось загрузить словарь: {e}")

    # ==================== ОКВЭД ====================
    def _load_okved_dict(self):
        """Загружает словарь ОКВЭД (dictionaries/okved.json)."""
        import json
        dict_path = os.path.join(os.path.dirname(__file__), 'dictionaries', 'okved.json')
        try:
            with open(dict_path, 'r', encoding='utf-8') as f:
                return json.load(f)
        except Exception as e:
            messagebox.showerror("Ошибка", f"Не удалось загрузить словарь ОКВЭД:\n{e}")
            return []

    def _select_main_okved(self):
        """Выбор основного ОКВЭД из словаря (всегда один)."""
        items = self._load_okved_dict()
        if not items:
            return
        current = self._company_entries.get('okved').get().strip() if 'okved' in self._company_entries else ''
        dialog = tk.Toplevel(self.root)
        dialog.title("Основной ОКВЭД")
        dialog.geometry("500x500")
        dialog.transient(self.root)
        dialog.grab_set()

        frame = ttk.Frame(dialog, padding=10)
        frame.pack(fill=tk.BOTH, expand=True)
        ttk.Label(frame, text="Выберите основной ОКВЭД (один):",
                  font=('Arial', 10, 'bold')).pack(anchor=tk.W, pady=(0, 5))

        canvas = tk.Canvas(frame, highlightthickness=0)
        scrollbar = ttk.Scrollbar(frame, orient=tk.VERTICAL, command=canvas.yview)
        check_frame = ttk.Frame(canvas)
        check_frame.bind("<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
        canvas.create_window((0, 0), window=check_frame, anchor="nw")
        canvas.configure(yscrollcommand=scrollbar.set)
        canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)

        # Радио-кнопки: основной всегда один
        var = tk.StringVar(value=current)
        for item in items:
            ttk.Radiobutton(check_frame, text=item, variable=var,
                            value=item).pack(anchor=tk.W, pady=1)

        btn_frame = ttk.Frame(dialog, padding=10)
        btn_frame.pack(fill=tk.X)

        def _apply():
            val = var.get()
            if val:
                entry = self._company_entries.get('okved')
                if entry:
                    entry.delete(0, tk.END)
                    entry.insert(0, val)
            dialog.destroy()

        ttk.Button(btn_frame, text="✅ Применить", command=_apply).pack(side=tk.RIGHT, padx=2)
        ttk.Button(btn_frame, text="Отмена", command=dialog.destroy).pack(side=tk.RIGHT, padx=2)
        dialog.wait_window()

    def _select_extra_okved(self):
        """Выбор дополнительных ОКВЭД из словаря (можно несколько)."""
        items = self._load_okved_dict()
        if not items:
            return
        self._select_from_dialog("Выбор дополнительных ОКВЭД", items, self.okved_extra_listbox)

    def _add_okved_manual(self):
        """Ручное добавление дополнительного ОКВЭД."""
        item = simpledialog.askstring("Добавить ОКВЭД", "Введите код ОКВЭД:")
        if item:
            self.okved_extra_listbox.insert(tk.END, item.strip())

    def _delete_okved_extra(self):
        sel = self.okved_extra_listbox.curselection()
        if sel:
            self.okved_extra_listbox.delete(sel[0])

    # ==================== ШАБЛОНЫ ====================
    def select_templates(self):
        folder = filedialog.askdirectory(title="Папка шаблонов")
        if folder:
            self.templates_var.set(folder)
            self._on_templates_changed()

    def _select_save_dir(self):
        folder = filedialog.askdirectory(title="Папка для результатов")
        if folder:
            self.save_var.set(folder)

    def _on_templates_changed(self, *_):
        """Обновить список шаблонов при смене папки."""
        folder = self.templates_var.get()
        self.template_listbox.delete(0, tk.END)
        self._template_files = []
        if folder and os.path.isdir(folder):
            for f in sorted(os.listdir(folder)):
                if f.endswith('.docx') and not f.endswith('_result.docx'):
                    self.template_listbox.insert(tk.END, f)
                    self._template_files.append(f)
            # Выделяем все по умолчанию
            self.template_listbox.selection_set(0, tk.END)
            self._selected_templates = set(self._template_files)
            self.log(f"Найдено шаблонов: {len(self._template_files)}")

    def _on_template_select(self, event=None):
        sel = self.template_listbox.curselection()
        self._selected_templates = {self._template_files[i] for i in sel}

    def _select_templates_all(self, select: bool):
        if select:
            self.template_listbox.selection_set(0, tk.END)
        else:
            self.template_listbox.selection_clear(0, tk.END)
        self._on_template_select()

    # ==================== ГЕНЕРАЦИЯ ====================
    def generate_all(self):
        """Генерация всех выбранных документов."""
        templates_dir = self.templates_var.get()
        output_dir = self.save_var.get()

        if not templates_dir or not os.path.isdir(templates_dir):
            messagebox.showerror("Ошибка", "Выберите папку с шаблонами")
            return
        if not output_dir:
            output_dir = os.path.join(
                os.path.dirname(os.path.abspath(__file__)),
                "Сгенерированные_документы"
            )
            self.save_var.set(output_dir)

        selected = self._selected_templates
        if not selected:
            messagebox.showwarning("Предупреждение", "Не выбран ни один шаблон")
            return

        # Собираем данные
        self._collect_company()
        self._collect_commission()
        self._collect_is()
        self._collect_employees()

        # Проверка
        if not self._validate_before_generate():
            return

        os.makedirs(output_dir, exist_ok=True)

        # Окно прогресса
        progress_win = tk.Toplevel(self.root)
        progress_win.title("Генерация документов")
        progress_win.geometry("350x100")
        progress_win.transient(self.root)
        progress_win.grab_set()
        progress_win.resizable(False, False)
        ttk.Label(progress_win, text="Генерация документов...",
                  font=('Arial', 10)).pack(pady=(15, 5))
        pb = ttk.Progressbar(progress_win, mode='indeterminate', length=300)
        pb.pack(pady=5)
        pb.start()

        def _close_progress():
            try:
                pb.stop()
                progress_win.grab_release()
                progress_win.destroy()
            except Exception:
                pass

        # Запускаем в отдельном потоке
        def _run():
            try:
                replacements = build_replacements(
                    company=self.model,
                    is_list=self.model.information_systems,
                    doc_number=self.doc_number_var.get(),
                    doc_date=self.doc_date_var.get(),
                    direction='187fz',
                )

                results = []
                for fname in selected:
                    template_path = os.path.join(templates_dir, fname)
                    if not os.path.isfile(template_path):
                        self.log(f"❌ Файл не найден: {fname}")
                        continue

                    base = os.path.splitext(fname)[0]
                    output_path = os.path.join(output_dir, f"{base}.docx")

                    try:
                        process_template(template_path, replacements, output_path, self.log)
                        results.append(output_path)
                    except Exception as e:
                        self.log(f"❌ {fname}: {e}")

                self.log(f"✅ Готово: {len(results)} из {len(selected)}")
                self.root.after(0, _close_progress)
                self.root.after(0, lambda: messagebox.showinfo(
                    "Готово", f"Сгенерировано {len(results)} документов\n"
                             f"Папка: {output_dir}"
                ))
            except Exception as e:
                self.log(f"❌ Ошибка: {e}")
                self.root.after(0, _close_progress)
                self.root.after(0, lambda: messagebox.showerror("Ошибка", str(e)))

        threading.Thread(target=_run, daemon=True).start()

    def _validate_before_generate(self):
        warnings = []
        if not self.model.full_name:
            warnings.append("Полное наименование организации")
        if not self.model.inn:
            warnings.append("ИНН")
        if not self.model.chief_fio:
            warnings.append("ФИО руководителя")
        if not self.model.information_systems:
            warnings.append("Нет информационных систем")
        if warnings:
            msg = "Не заполнены:\n" + "\n".join(warnings)
            return messagebox.askyesno("Внимание", msg + "\n\nПродолжить?")
        return True

    # ==================== СКЛОНЕНИЕ ====================
    def _update_company_declensions(self):
        name = self.company_name_entry.get().strip()
        if not name:
            return
        self.model.full_name = name
        self.model.update_declensions(inflect_name)
        idx_map = {'nomn': 1, 'gent': 2, 'datv': 3, 'accs': 4, 'ablt': 5, 'loct': 6}
        for code, entry in self.decl_entries.items():
            entry.delete(0, tk.END)
            entry.insert(0, getattr(self.model, f"company_name_{idx_map[code]}", ""))
        self.log("✅ Склонения полного названия обновлены")

    def _update_short_declensions(self):
        name = self.short_name_entry.get().strip()
        if not name:
            return
        self.model.short_name = name
        self.model.update_short_declensions(inflect_name)
        idx_map = {'nomn': 1, 'gent': 2, 'datv': 3, 'accs': 4, 'ablt': 5, 'loct': 6}
        for code, entry in self.short_decl_entries.items():
            entry.delete(0, tk.END)
            entry.insert(0, getattr(self.model, f"short_name_{idx_map[code]}", ""))
        self.log("✅ Склонения краткого названия обновлены")

    # ==================== ИМПОРТ ====================
    def select_import_settings(self):
        """Выбор файла настроек импорта (JSON-словарь)."""
        from .import_settings import default_import_settings_path, ensure_default_file
        path = filedialog.askopenfilename(
            title="Файл настроек импорта (что и откуда брать)",
            filetypes=[("Настройки импорта", "*.json")]
        )
        if not path:
            self.log(f"Настройки импорта: по умолчанию ({default_import_settings_path()})")
            self.import_settings_path = None
            return
        self.import_settings_path = path
        self.log(f"Настройки импорта: {path}")

    def import_from_survey(self):
        path = filedialog.askopenfilename(
            title="Выберите опросный лист (187-ФЗ)",
            filetypes=[("Excel", "*.xlsx *.xls")]
        )
        if not path:
            return
        self.log(f"Импорт: {path}")

        # Импорт в отдельном потоке
        def _run():
            try:
                from .importer import import_187fz
                data = import_187fz(path, self.log, settings_path=self.import_settings_path)
                if data:
                    self._apply_imported_data(data)
            except ImportError:
                self.log("❌ Модуль импорта не найден")
            except Exception as e:
                self.log(f"❌ Ошибка импорта: {e}")

        threading.Thread(target=_run, daemon=True).start()

    def _apply_imported_data(self, data: dict):
        def _apply():
            # Название организации
            if data.get('fullName'):
                self.company_name_entry.delete(0, tk.END)
                self.company_name_entry.insert(0, data['fullName'])
                self.model.full_name = data['fullName']
            if data.get('shortName'):
                self.short_name_entry.delete(0, tk.END)
                self.short_name_entry.insert(0, data['shortName'])
                self.model.short_name = data['shortName']

            # Адрес и реквизиты (маппинг camelCase → snake_case)
            addr_map = {
                'address': 'addressLegal',
                'city_name': None,
                'inn': 'inn',
                'kpp': 'kpp',
                'ogrn': 'ogrn',
                'ogrn_date': 'ogrnDate',
            }
            for entry_key, data_key in addr_map.items():
                entry = self._company_entries.get(entry_key)
                if not entry:
                    continue
                val = ''
                if data_key and data.get(data_key):
                    val = data[data_key]
                elif data.get(entry_key):
                    val = data[entry_key]
                if val:
                    entry.delete(0, tk.END)
                    entry.insert(0, val)

            # Автоопределение города из адреса после импорта
            self._on_address_changed()

            # Должностные лица (маппинг)
            officials_map = {
                'chief_position': 'chiefPosition',
                'chief_fio': 'chiefFio',
                'responsible_position': 'responsiblePosition',
                'responsible_fio': 'responsibleFio',
                'admin_position': ('administratorPosition', 'ispdnPosition'),
                'admin_fio': ('administratorFio', 'ispdnFio'),
            }
            for entry_key, data_keys in officials_map.items():
                entry = self._official_entries.get(entry_key)
                if not entry:
                    continue
                val = ''
                if isinstance(data_keys, tuple):
                    for dk in data_keys:
                        if data.get(dk):
                            val = data[dk]
                            break
                elif data.get(data_keys):
                    val = data[data_keys]
                if val:
                    entry.delete(0, tk.END)
                    entry.insert(0, val)

            # Номер и дата документа
            if data.get('contractNumber'):
                self.doc_number_var.set(data['contractNumber'])
            if data.get('contractDate'):
                self.doc_date_var.set(data['contractDate'])

            # Финансовые показатели (прибыль, налог)
            for entry_key, data_keys in [('profit', 'profit'), ('profit_tax', 'profitTax')]:
                entry = self._company_entries.get(entry_key)
                if entry and data.get(data_keys):
                    entry.delete(0, tk.END)
                    entry.insert(0, data[data_keys])

            # Наименование структурного подразделения по безопасности
            if data.get('ispdnDepartment'):
                self.model.security_department = data['ispdnDepartment']

            # Доменное имя и внешний сетевой адрес
            if data.get('domainName'):
                entry = self._company_entries.get('domain_name')
                if entry:
                    entry.delete(0, tk.END)
                    entry.insert(0, data['domainName'])

            # Доменное имя и внешний сетевой адрес
            if data.get('domainName'):
                entry = self._company_entries.get('domain_name')
                if entry:
                    entry.delete(0, tk.END)
                    entry.insert(0, data['domainName'])

            # ОКВЭД (основной и дополнительные)
            if data.get('okved'):
                entry = self._company_entries.get('okved')
                if entry:
                    from .verify import resolve_okved
                    from .api_settings import load_api_settings as _las
                    _tok = (_las().get('dadata_token') or '')
                    entry.delete(0, tk.END)
                    entry.insert(0, resolve_okved(data['okved'], _tok))
            if data.get('okvedExtra'):
                if hasattr(self, 'okved_extra_listbox'):
                    from .verify import resolve_okved
                    from .api_settings import load_api_settings as _las2
                    _tok2 = (_las2().get('dadata_token') or '')
                    self.okved_extra_listbox.delete(0, tk.END)
                    extra = data['okvedExtra']
                    if isinstance(extra, str):
                        import re as _re
                        parts = [p.strip() for p in _re.split(r'[;,\n]+', extra) if p.strip()]
                    else:
                        parts = [str(p).strip() for p in extra if str(p).strip()]
                    for p in parts:
                        self.okved_extra_listbox.insert(tk.END, resolve_okved(p, _tok2))

            # Объекты КИИ
            if data.get('informationSystems'):
                self.model.information_systems.clear()
                for is_data in data['informationSystems']:
                    isys = InformationSystem(
                        name=is_data.get('name', ''),
                        description=is_data.get('description', ''),
                        is_local_network=is_data.get('is_has_lan', is_data.get('is_local_network', False)),
                        is_internet=is_data.get('isInternet', False),
                        category=is_data.get('category', ''),
                        defense_tools_list=is_data.get('defense_tools', []),
                        users_list=is_data.get('usersList', []),
                        purpose=is_data.get('purpose', ''),
                        structure=is_data.get('structure', ''),
                        software=is_data.get('software', ''),
                        sphere=is_data.get('sphere', ''),
                        object_type=is_data.get('object_type', ''),
                        architecture=is_data.get('architecture', ''),
                        typical_object=is_data.get('typical_object', ''),
                        subject_name=is_data.get('subject_name', ''),
                        subject_address=is_data.get('subject_address', ''),
                        subject_element=is_data.get('subject_element', ''),
                        subject_inn=is_data.get('subject_inn', ''),
                        subject_kpp=is_data.get('subject_kpp', ''),
                        net_category=is_data.get('net_category', ''),
                        net_operator=is_data.get('net_operator', ''),
                        net_purpose=is_data.get('net_purpose', ''),
                        net_method=is_data.get('net_method', ''),
                        hw_sw=is_data.get('hw_sw', ''),
                        os_sw=is_data.get('os_sw', ''),
                        app_sw=is_data.get('app_sw', ''),
                        balance_value=is_data.get('balanceValue', is_data.get('balance_value', '')),
                        attacker_category=is_data.get('attacker_category', ''),
                        threats=is_data.get('threats', ''),
                        incidents=is_data.get('incidents', ''),
                        criteria_values=is_data.get('criteria_values', ''),
                        criteria_justification=is_data.get('criteria_justification', ''),
                        org_measures=is_data.get('org_measures', ''),
                        tech_measures=is_data.get('tech_measures', ''),
                    )
                    self.model.information_systems.append(isys)
                self._refresh_is_listbox()
                if self.model.information_systems:
                    self.is_listbox.selection_set(0)
                    self._on_is_select()

            # Комиссия
            if data.get('commission'):
                self.model.commission.clear()
                # Очистим UI
                for row_frame, _, _, _ in self.commission_entries:
                    row_frame.destroy()
                self.commission_entries.clear()
                for m in data['commission']:
                    self.model.commission.append(
                        CommissionMember(
                            role=m.get('role', 'Член комиссии'),
                            position=m.get('position', ''),
                            fio=m.get('fio', ''),
                        )
                    )
                    self._add_commission_row(
                        m.get('role', 'Член комиссии'),
                        m.get('position', ''),
                        m.get('fio', ''),
                    )

            # Сотрудники с доступом
            if data.get('employeesAccess'):
                self.model.employees_access.clear()
                if hasattr(self, '_employee_entries'):
                    for row_frame, _, _ in self._employee_entries:
                        row_frame.destroy()
                    self._employee_entries.clear()
                for emp in data['employeesAccess']:
                    fio = str(emp.get('fio', '')).strip()
                    pos = str(emp.get('position', '')).strip()
                    if fio.lower().startswith('формат:') or fio.lower().startswith('пример:'):
                        continue
                    if not fio and not pos:
                        continue
                    self.model.employees_access.append({
                        'position': pos,
                        'fio': fio,
                    })
                    self._add_employee_row(pos, fio)
                self._update_emp_count()

            self.log("✅ Импорт применён")
        self.root.after(0, _apply)

    # ==================== ФАЙЛОВЫЕ ОПЕРАЦИИ ====================
    def new_data(self):
        if self.model.full_name:
            if not messagebox.askyesno("Новое", "Текущие данные будут потеряны. Продолжить?"):
                return
        self._init_company()
        self.current_file = None
        self._company_to_gui()
        self.log("Новые данные")

    def open_data(self):
        filename = filedialog.askopenfilename(filetypes=[("JSON", "*.json")])
        if filename:
            try:
                with open(filename, 'r', encoding='utf-8') as f:
                    data = json.load(f)
                self.model = Company.from_dict(data)
                self.current_file = filename
                self._company_to_gui()
                self.log(f"Загружено: {filename}")
            except Exception as e:
                messagebox.showerror("Ошибка", str(e))

    def save_data(self):
        if self.current_file:
            self._save_to(self.current_file)
        else:
            self.save_as_data()

    def save_as_data(self):
        filename = filedialog.asksaveasfilename(
            defaultextension=".json", filetypes=[("JSON", "*.json")]
        )
        if filename:
            self._save_to(filename)

    def _save_to(self, filename):
        self._collect_company()
        self._collect_commission()
        self._collect_is()
        self._collect_employees()
        try:
            with open(filename, 'w', encoding='utf-8') as f:
                json.dump(self.model.to_dict(), f, ensure_ascii=False, indent=2)
            self.current_file = filename
            self.log(f"Сохранено: {filename}")
        except Exception as e:
            messagebox.showerror("Ошибка", str(e))

    # ==================== АВТОСОХРАНЕНИЕ ====================
    def _start_autosave_timer(self):
        self._autosave()
        self._autosave_timer_id = self.root.after(120000, self._start_autosave_timer)

    def _stop_autosave_timer(self):
        if hasattr(self, '_autosave_timer_id') and self._autosave_timer_id:
            try:
                self.root.after_cancel(self._autosave_timer_id)
            except:
                pass

    def _autosave(self):
        self._collect_company()
        self._collect_commission()
        self._collect_is()
        self._collect_employees()
        try:
            os.makedirs(os.path.dirname(self._autosave_path), exist_ok=True)
            with open(self._autosave_path, 'w', encoding='utf-8') as f:
                json.dump(self.model.to_dict(), f, ensure_ascii=False, indent=2)
        except Exception:
            pass

    def _load_autosave(self):
        try:
            if not os.path.isfile(self._autosave_path):
                return
            mtime = os.path.getmtime(self._autosave_path)
            if datetime.now().timestamp() - mtime > 86400:
                return
            with open(self._autosave_path, 'r', encoding='utf-8') as f:
                data = json.load(f)
            self.model = Company.from_dict(data)
            self._company_to_gui()
            self.log("📂 Данные восстановлены из автосохранения")
        except Exception:
            pass

    def _on_close(self):
        self._stop_autosave_timer()
        self._autosave()
        self.root.destroy()

    # ==================== КОНТЕКСТНОЕ МЕНЮ ====================
    def _enable_context_menu(self):
        def _show_menu(event):
            widget = event.widget
            if not isinstance(widget, (tk.Entry, ttk.Entry, tk.Text, tk.Listbox)):
                return
            menu = tk.Menu(self.root, tearoff=0)
            def _copy():
                try:
                    text = ""
                    if isinstance(widget, (tk.Entry, ttk.Entry)):
                        text = widget.selection_get() if widget.selection_present() else ""
                    elif isinstance(widget, tk.Text):
                        text = widget.selection_get() if widget.tag_ranges(tk.SEL) else ""
                    if text:
                        self.root.clipboard_clear()
                        self.root.clipboard_append(text)
                except:
                    pass
            def _paste():
                try:
                    text = self.root.clipboard_get()
                    if not text: return
                    if isinstance(widget, (tk.Entry, ttk.Entry)):
                        try: widget.delete(tk.SEL_FIRST, tk.SEL_LAST)
                        except: pass
                        widget.insert(tk.INSERT, text)
                    elif isinstance(widget, tk.Text):
                        try:
                            if widget.tag_ranges(tk.SEL):
                                widget.delete(tk.SEL_FIRST, tk.SEL_LAST)
                        except: pass
                        widget.insert(tk.INSERT, text)
                except:
                    pass
            menu.add_command(label="Копировать", command=_copy)
            menu.add_command(label="Вставить", command=_paste)
            menu.add_separator()
            menu.add_command(label="Выделить всё", command=lambda: widget.select_range(0, tk.END) if hasattr(widget, 'select_range') else None)
            menu.tk_popup(event.x_root, event.y_root)
        self.root.bind_all('<Button-3>', _show_menu, add='+')

    # ==================== ВСПОМОГАТЕЛЬНЫЕ МЕТОДЫ ====================
    def log(self, message: str):
        ts = datetime.now().strftime("%H:%M:%S")
        try:
            self.log_text.configure(state='normal')
            self.log_text.insert(tk.END, f"[{ts}] {message}\n")
            self.log_text.see(tk.END)
            self.log_text.configure(state='disabled')
        except Exception:
            pass
        # Дублируем в файловый журнал
        try:
            with open(self._log_path, 'a', encoding='utf-8') as f:
                f.write(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] {message}\n")
        except Exception:
            pass
        self.status_var.set(message)

    def _export_logs(self):
        """Экспорт лога в файл."""
        filename = filedialog.asksaveasfilename(
            title="Экспорт логов",
            defaultextension=".log",
            initialfile=f"dokogen_log_{datetime.now().strftime('%Y%m%d_%H%M')}.log",
            filetypes=[("Лог-файл", "*.log"), ("Текстовый файл", "*.txt"), ("Все файлы", "*.*")]
        )
        if not filename:
            return
        try:
            content = self.log_text.get('1.0', tk.END)
            with open(filename, 'w', encoding='utf-8') as f:
                f.write(content)
            self.log(f"💾 Лог экспортирован: {filename}")
        except Exception as e:
            messagebox.showerror("Ошибка", f"Не удалось экспортировать лог: {e}")

    def show_about(self):
        messagebox.showinfo("О программе",
                            "ДоКоГеНеРаТоР — 187-ФЗ\n"
                            "Версия 1.0\n"
                            "Генератор документов из шаблонов")

    def show_variables(self):
        """Показ переменных шаблонов — теперь через скачивание Word-документа."""
        self.download_variables_doc()

    # ---------- СПРАВКА: СКАЧИВАНИЕ ИНСТРУКЦИЙ / ОПРОСНИКА ----------
    def _save_resource(self, source_path, default_name, title):
        """Копирует ресурс (docx/xlsx) в выбранное пользователем место."""
        if not source_path or not os.path.exists(source_path):
            messagebox.showerror("Справка", f"Файл не найден:\n{source_path}")
            return
        filename = filedialog.asksaveasfilename(
            title=title,
            defaultextension=os.path.splitext(default_name)[1],
            initialfile=default_name,
            filetypes=[("Документ", "*.docx"), ("Все файлы", "*.*")]
        )
        if not filename:
            return
        try:
            import shutil
            shutil.copy(source_path, filename)
            self.log(f"💾 Сохранено: {filename}")
            messagebox.showinfo("Справка", f"Файл сохранён:\n{filename}")
        except Exception as e:
            messagebox.showerror("Ошибка", f"Не удалось сохранить: {e}")

    def download_user_manual(self):
        from .help_docs import ensure_docs, user_manual_path
        ensure_docs()
        self._save_resource(user_manual_path(), "Инструкция_пользователя.docx", "Сохранить инструкцию пользователя")

    def download_admin_manual(self):
        from .help_docs import ensure_docs, admin_manual_path
        ensure_docs()
        self._save_resource(admin_manual_path(), "Инструкция_администратора.docx", "Сохранить инструкцию администратора")

    def download_variables_doc(self):
        from .help_docs import ensure_docs, variables_reference_path
        ensure_docs()
        self._save_resource(variables_reference_path(), "Переменные_шаблонов.docx", "Сохранить справочник переменных")

    def download_survey(self):
        from .help_docs import ensure_docs, survey_path
        ensure_docs()
        self._save_resource(survey_path(), "Опросный_лист_187_ФЗ.xlsx", "Сохранить пустой опросник")


def main():
    """Точка входа в приложение."""
    root = tk.Tk()
    app = DokoGenApp(root)
    root.mainloop()
