Исправь ошибки и напиши полный корректный код.
"""
Автоматизация техдокументации с GUI:

- Загрузка Excel (Объект, Свойство, Значение)
- Формирование "Тип товара" и "Артикул"
- Управление корзиной
- Управление плейсхолдерами
- Выбор шаблона с предпросмотром
- Профили настроек (сохранение/загрузка)
- Генерация DOCX и PDF (через MS Word + pywin32)
- Логирование с экспортом
- Предпросмотр итогового документа в MS Word
"""

import os
import json
import traceback
import tempfile
from datetime import datetime
from io import BytesIO
import subprocess

import pandas as pd
from PIL import Image, ImageTk
from docx import Document
from docx.shared import Pt, Inches
from docx.enum.text import WD_ALIGN_PARAGRAPH
import tkinter as tk
from tkinter import ttk, filedialog, messagebox, simpledialog

try:
    import win32com.client
    WIN32COM_AVAILABLE = True
except ImportError:
    WIN32COM_AVAILABLE = False


WORKDIR = os.getcwd()
TEMPLATES_DIR = os.path.join(WORKDIR, "templates")
IMAGES_DIR = os.path.join(WORKDIR, "images")
LOGS_DIR = os.path.join(WORKDIR, "logs")
CONFIG_DIR = os.path.join(WORKDIR, "config")
PROFILES_DIR = os.path.join(CONFIG_DIR, "profiles")

for d in (TEMPLATES_DIR, IMAGES_DIR, LOGS_DIR, CONFIG_DIR, PROFILES_DIR):
    os.makedirs(d, exist_ok=True)

SETTINGS_FILE = os.path.join(CONFIG_DIR, "settings.json")
PLACEHOLDERS_FILE = os.path.join(CONFIG_DIR, "placeholders.json")
MANDATORY_COLS = ['Объект', 'Свойство', 'Значение']
IMAGE_FORMATS = ['JPG', 'PNG', 'BMP']


class Logger:
    def __init__(self):
        self.history = []

    def log(self, msg):
        ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        record = f"[{ts}] {msg}"
        self.history.append(record)
        print(record)

    def get_text(self):
        return "\n".join(self.history)

    def save_to_file(self, path):
        try:
            with open(path, 'w', encoding='utf-8') as f:
                f.write(self.get_text())
            self.log(f"Лог сохранён: {path}")
        except Exception as e:
            self.log(f"Ошибка сохранения лога: {e}")


logger = Logger()


class Settings:
    def __init__(self):
        self.data = {
            "table_style": {"header_bold": True, "alignment": "center", "font_name": "Calibri", "font_size": 11},
            "image": {"max_width_inches": 4, "format": IMAGE_FORMATS, "compression": 75},
            "export": {"format": "docx", "pdf_password": "", "compress_images": True, "template": None}
        }
        self.load()

    def load(self):
        if os.path.exists(SETTINGS_FILE):
            try:
                with open(SETTINGS_FILE, 'r', encoding='utf-8') as f:
                    d = json.load(f)
                self.data.update(d)
                exp = self.data.get('export', {})
                if 'format' not in exp:
                    exp['format'] = 'docx'
                if 'pdf_password' not in exp:
                    exp['pdf_password'] = ''
                if 'compress_images' not in exp:
                    exp['compress_images'] = True
                self.data['export'] = exp
                logger.log("Настройки загружены")
            except Exception as e:
                logger.log(f"Ошибка загрузки настроек: {e}")

    def save(self):
        try:
            with open(SETTINGS_FILE, 'w', encoding='utf-8') as f:
                json.dump(self.data, f, ensure_ascii=False, indent=4)
            logger.log("Настройки сохранены")
        except Exception as e:
            logger.log(f"Ошибка сохранения настроек: {e}")

    def save_profile(self, filename):
        try:
            with open(os.path.join(PROFILES_DIR, filename), 'w', encoding='utf-8') as f:
                json.dump(self.data, f, ensure_ascii=False, indent=4)
            logger.log(f"Профиль сохранён: {filename}")
            return True
        except Exception as e:
            logger.log(f"Ошибка сохранения профиля: {e}")
            return False

    def load_profile(self, filename):
        try:
            with open(os.path.join(PROFILES_DIR, filename), 'r', encoding='utf-8') as f:
                d = json.load(f)
            self.data.update(d)
            logger.log(f"Профиль загружен: {filename}")
            return True
        except Exception as e:
            logger.log(f"Ошибка загрузки профиля: {e}")
            return False


settings = Settings()


class Placeholders:
    def __init__(self):
        self.placeholders = {}
        self.load()

    def load(self):
        if os.path.isfile(PLACEHOLDERS_FILE):
            try:
                with open(PLACEHOLDERS_FILE, 'r', encoding='utf-8') as f:
                    self.placeholders = json.load(f)
                logger.log("Плейсхолдеры загружены")
            except Exception as e:
                logger.log(f"Ошибка загрузки плейсхолдеров: {e}")

    def save(self):
        try:
            with open(PLACEHOLDERS_FILE, 'w', encoding='utf-8') as f:
                json.dump(self.placeholders, f, ensure_ascii=False, indent=4)
            logger.log("Плейсхолдеры сохранены")
        except Exception as e:
            logger.log(f"Ошибка сохранения плейсхолдеров: {e}")


placeholders = Placeholders()


def extract_article_full(obj):
    if not isinstance(obj, str) or not obj.strip():
        return ""
    low = obj.lower()
    idx = low.find("idis")
    if idx >= 0:
        return obj[idx + 4 :].strip()
    words = obj.strip().split()
    if len(words) <= 2:
        return obj.strip()
    return " ".join(words[-3:]).strip()


def extract_product_type(obj):
    if not isinstance(obj, str) or not obj.strip():
        return ""
    low = obj.lower()
    idx = low.find("idis")
    if idx >= 0:
        part = obj[:idx].strip()
        if part:
            return part
        return "Неизвестно"
    words = obj.strip().split()
    if len(words) <= 3:
        return " ".join(words)
    return " ".join(words[:3])


def extract_param(prop):
    if not isinstance(prop, str):
        return ""
    return prop.split("_")[-1].strip()


def load_and_transform_excel_grouped(path):
    logger.log(f"Загрузка файла: {path}")
    df = pd.read_excel(path, engine="openpyxl")
    missing = [c for c in MANDATORY_COLS if c not in df.columns]
    if missing:
        raise ValueError(f"Отсутствуют колонки: {missing}")

    df["Тип товара"] = df["Объект"].apply(extract_product_type)
    df["Артикул"] = df["Объект"].apply(extract_article_full)
    df["Параметр"] = df["Свойство"].apply(extract_param)
    df["Значение"] = df["Значение"].astype(str)
    df = df[(df["Артикул"] != "") & (df["Параметр"] != "")].reset_index(drop=True)

    grouped = {}
    for art, gdf in df.groupby("Артикул"):
        obj_full = gdf["Объект"].iloc[0]
        prod_type = gdf["Тип товара"].iloc[0]

        sections = []
        current_header = None
        current_params = []

        for _, row in gdf.iterrows():
            if row["Значение"].strip().lower() == "<раздел>":
                if current_header or current_params:
                    sections.append({"header": current_header, "params": current_params})
                current_header = row["Параметр"]
                current_params = []
            else:
                current_params.append({"parameter": row["Параметр"], "value": row["Значение"]})

        if current_header or current_params:
            sections.append({"header": current_header, "params": current_params})

        if not sections:
            sections = [
                {
                    "header": None,
                    "params": [{"parameter": r["Параметр"], "value": r["Значение"]} for _, r in gdf.iterrows()],
                }
            ]

        grouped[art] = {"object_full": obj_full, "product_type": prod_type, "sections": sections}
    logger.log(f"Данные сгруппированы по артикулам: {len(grouped)}")
    return grouped


def pil_image_to_tk(img, maxsize=(300, 250)):
    im = img.copy()
    im.thumbnail(maxsize, Image.ANTIALIAS)
    return ImageTk.PhotoImage(im)


def get_image_path(filename):
    base, ext = os.path.splitext(filename)
    ext = ext[1:].upper()
    if ext in IMAGE_FORMATS:
        p = os.path.join(IMAGES_DIR, filename)
        if os.path.isfile(p):
            return p
    for fmt in IMAGE_FORMATS:
        p = os.path.join(IMAGES_DIR, base + "." + fmt.lower())
        if os.path.isfile(p):
            return p
    return None


def docx_replace_placeholder(doc, key, value):
    ph = "{{" + key + "}}"
    for p in doc.paragraphs:
        if ph in p.text:
            for r in p.runs:
                if ph in r.text:
                    r.text = r.text.replace(ph, str(value))
    for tbl in doc.tables:
        for row in tbl.rows:
            for cell in row.cells:
                for p in cell.paragraphs:
                    if ph in p.text:
                        for r in p.runs:
                            if ph in r.text:
                                r.text = r.text.replace(ph, str(value))


def add_section_table(doc, section, style):
    if section["header"]:
        p = doc.add_paragraph(section["header"], style="Heading3")
        run = p.runs[0]
        run.bold = True
        run.font.size = Pt(style.get("font_size", 12))
        p.alignment = WD_ALIGN_PARAGRAPH.LEFT
    tbl = doc.add_table(rows=1, cols=2)
    tbl.style = "Table Grid"
    hdr_cells = tbl.rows[0].cells
    hdr_cells[0].text = "Параметр"
    hdr_cells[1].text = "Значение"
    for c in hdr_cells:
        p = c.paragraphs[0]
        for r in p.runs:
            r.bold = style.get("header_bold", True)
            r.font.name = style.get("font_name", "Calibri")
            r.font.size = Pt(style.get("font_size", 11))
        alignment = style.get("alignment", "center")
        p.alignment = {
            "left": WD_ALIGN_PARAGRAPH.LEFT,
            "right": WD_ALIGN_PARAGRAPH.RIGHT,
            "center": WD_ALIGN_PARAGRAPH.CENTER,
        }[alignment]
    for param in section["params"]:
        row_cells = tbl.add_row().cells
        row_cells[0].text = str(param["parameter"])
        row_cells[1].text = str(param["value"])
        for i in (0, 1):
            p = row_cells[i].paragraphs[0]
            for r in p.runs:
                r.font.name = style.get("font_name", "Calibri")
                r.font.size = Pt(style.get("font_size", 11))
            p.alignment = {
                "left": WD_ALIGN_PARAGRAPH.LEFT,
                "right": WD_ALIGN_PARAGRAPH.RIGHT,
                "center": WD_ALIGN_PARAGRAPH.CENTER,
            }[style.get("alignment", "center")]


def insert_image(doc_or_cell, img_path, max_w_inch):
    try:
        if not os.path.exists(img_path):
            logger.log(f"Image not found: {img_path}")
            return
        img = Image.open(img_path)
        dpi = img.info.get("dpi", (96, 96))[0]
        w_px, _ = img.size
        w_inch = w_px / dpi
        if w_inch > max_w_inch:
            w_inch = max_w_inch
        doc_or_cell.add_picture(img_path, width=Inches(w_inch))
    except Exception as e:
        logger.log(f"Image insert error {img_path}: {e}")


def convert_docx_to_pdf(docx_path, pdf_path):
    if not WIN32COM_AVAILABLE:
        raise RuntimeError("Для экспорта в PDF нужен MS Word и pywin32")
    word = win32com.client.DispatchEx("Word.Application")
    try:
        word.Visible = False
        doc = word.Documents.Open(docx_path)
        doc.ExportAsFixedFormat(OutputFileName=pdf_path, ExportFormat=17)
        doc.Close(False)
        logger.log(f"PDF создан: {pdf_path}")
    finally:
        word.Quit()


class App(tk.Tk):
    def __init__(self):
        super().__init__()

        self.title("Автоматизация техдокументации")
        self.geometry("1350x820")

        self.grouped_data = {}
        self.filtered_df = pd.DataFrame()
        self.basket_df = pd.DataFrame(
            columns=["Объект", "Тип товара", "Артикул", "Параметр", "Значение"]
        )

        self.user_placeholders = placeholders.placeholders.copy()
        self.templates = []
        self.current_template = None

        self.selected_profile = None

        self.create_widgets()
        self.create_menu()
        self.bind_shortcuts()
        self.load_templates()
        self.load_profiles()
        self.load_placeholders_to_tree()
        self.load_settings_to_gui()
        self.update_log_ui()

        logger.log("Приложение запущено")

    def create_widgets(self):
        self.tabs = ttk.Notebook(self)
        self.tabs.pack(fill="both", expand=True)

        # -- Данные --
        tab_data = ttk.Frame(self.tabs)
        self.tabs.add(tab_data, text="Данные")

        fsearch = ttk.Frame(tab_data)
        fsearch.pack(fill="x", padx=5, pady=3)
        ttk.Label(fsearch, text="Поиск по артикулу:").pack(side="left")
        self.var_quick_search = tk.StringVar()
        ent_qs = ttk.Entry(fsearch, textvariable=self.var_quick_search)
        ent_qs.pack(side="left", fill="x", expand=True, padx=5)
        ent_qs.bind("<KeyRelease>", self.quick_search)

        ffilter = ttk.Frame(tab_data)
        ffilter.pack(fill="x", padx=5, pady=3)
        ttk.Label(ffilter, text="Фильтр по колонке:").pack(side="left")
        self.combo_filter_col = ttk.Combobox(ffilter, state="readonly", width=20)
        self.combo_filter_col.pack(side="left", padx=5)
        ttk.Label(ffilter, text="Оператор:").pack(side="left")
        self.combo_filter_op = ttk.Combobox(
            ffilter,
            state="readonly",
            values=["содержит", "равно", "начинается с", "оканчивается на"],
            width=15,
        )
        self.combo_filter_op.set("содержит")
        self.combo_filter_op.pack(side="left", padx=5)
        self.var_filter_val = tk.StringVar()
        ent_fv = ttk.Entry(ffilter, textvariable=self.var_filter_val)
        ent_fv.pack(side="left", fill="x", expand=True, padx=5)
        ent_fv.bind("<KeyRelease>", self.advanced_filter)
        self.combo_filter_op.bind("<<ComboboxSelected>>", lambda e: self.advanced_filter())
        self.combo_filter_col.bind("<<ComboboxSelected>>", lambda e: self.advanced_filter())
        ttk.Button(ffilter, text="Очистить", command=self.clear_filter).pack(side="left", padx=5)

        tbl_frame = ttk.Frame(tab_data)
        tbl_frame.pack(fill="both", expand=True, padx=5, pady=5)
        self.tree_data = ttk.Treeview(tbl_frame, show="headings", selectmode="extended")
        self.tree_data.pack(side="left", fill="both", expand=True)
        self.tree_data.bind("<Double-1>", self.add_selected_to_basket)
        self.tree_data.bind("<Button-3>", self.context_menu_data)

        scr_y = ttk.Scrollbar(tbl_frame, orient="vertical", command=self.tree_data.yview)
        scr_y.pack(side="left", fill="y")
        scr_x = ttk.Scrollbar(tbl_frame, orient="horizontal", command=self.tree_data.xview)
        scr_x.pack(side="bottom", fill="x")
        self.tree_data.configure(yscrollcommand=scr_y.set, xscrollcommand=scr_x.set)

        btn_frame = ttk.Frame(tab_data)
        btn_frame.pack(fill="x", padx=5, pady=5)
        ttk.Button(btn_frame, text="Загрузить Excel", command=self.load_excel_dialog).pack(side="left")
        ttk.Button(btn_frame, text="Добавить в корзину", command=self.add_selected_to_basket).pack(
            side="left", padx=5
        )

        # -- Корзина --
        tab_basket = ttk.Frame(self.tabs)
        self.tabs.add(tab_basket, text="Корзина")

        basket_frame = ttk.Frame(tab_basket)
        basket_frame.pack(fill="both", expand=True, padx=5, pady=5)
        self.tree_basket = ttk.Treeview(basket_frame, show="headings", selectmode="extended")
        self.tree_basket.pack(side="left", fill="both", expand=True)
        self.tree_basket.bind("<Delete>", lambda e: self.remove_selected_from_basket())
        self.tree_basket.bind("<Button-3>", self.context_menu_basket)

        sb_basket_y = ttk.Scrollbar(basket_frame, orient="vertical", command=self.tree_basket.yview)
        sb_basket_y.pack(side="left", fill="y")
        sb_basket_x = ttk.Scrollbar(basket_frame, orient="horizontal", command=self.tree_basket.xview)
        sb_basket_x.pack(side="bottom", fill="x")
        self.tree_basket.configure(yscrollcommand=sb_basket_y.set, xscrollcommand=sb_basket_x.set)

        ctrl_basket = ttk.Frame(tab_basket)
        ctrl_basket.pack(fill="x", padx=5, pady=5)
        ttk.Button(ctrl_basket, text="Удалить выделенные", command=self.remove_selected_from_basket).pack(side="left", padx=5)
        ttk.Button(ctrl_basket, text="Очистить корзину", command=self.clear_basket).pack(side="left", padx=5)
        ttk.Button(ctrl_basket, text="Показать текст корзины", command=self.show_basket_text).pack(side="left", padx=5)

        self.txt_basket_text = tk.Text(tab_basket, height=10)
        self.txt_basket_text.pack(fill="both", padx=5, pady=5)

        # -- Настройки и генерация --
        tab_set_gen = ttk.Frame(self.tabs)
        self.tabs.add(tab_set_gen, text="Настройки и Генерация")

        # Профили настроек
        prof_frame = ttk.LabelFrame(tab_set_gen, text="Профили настроек")
        prof_frame.pack(fill='x', padx=10, pady=5)
        self.combo_profiles = ttk.Combobox(prof_frame, state='readonly')
        self.combo_profiles.pack(side='left', fill='x', expand=True, padx=5, pady=5)
        self.combo_profiles.bind("<<ComboboxSelected>>", self.on_profile_change)
        ttk.Button(prof_frame, text="Загрузить профиль", command=self.load_profile).pack(side='left', padx=5)
        ttk.Button(prof_frame, text="Сохранить профиль", command=self.save_profile).pack(side='left', padx=5)
        ttk.Button(prof_frame, text="Обновить список", command=self.load_profiles).pack(side='left', padx=5)

        # Шаблоны
        ttk.Label(tab_set_gen, text="Выбор шаблона:").pack(anchor='w', padx=10)
        self.combo_templates = ttk.Combobox(tab_set_gen, state='readonly', width=70)
        self.combo_templates.pack(fill='x', padx=10, pady=5)
        self.combo_templates.bind("<<ComboboxSelected>>", self.on_template_change)
        ttk.Button(tab_set_gen, text="Предпросмотр шаблона", command=self.preview_template).pack(anchor='w', padx=10)

        # Плейсхолдеры
        ttk.Label(tab_set_gen, text="Плейсхолдеры:").pack(anchor='w', padx=10, pady=3)
        self.tree_placeholders = ttk.Treeview(tab_set_gen, columns=("name", "value"), show='headings', height=10)
        self.tree_placeholders.pack(fill='both', padx=10, pady=3, expand=True)
        self.tree_placeholders.heading("name", text="Имя")
        self.tree_placeholders.heading("value", text="Значение")
        self.tree_placeholders.column("name", width=200)
        self.tree_placeholders.column("value", width=400)

        ph_btn_frame = ttk.Frame(tab_set_gen)
        ph_btn_frame.pack(fill='x', padx=10, pady=1)
        ttk.Button(ph_btn_frame, text="Добавить", command=self.add_placeholder).pack(side='left', padx=5)
        ttk.Button(ph_btn_frame, text="Редактировать", command=self.edit_placeholder).pack(side='left', padx=5)
        ttk.Button(ph_btn_frame, text="Удалить", command=self.delete_placeholder).pack(side='left', padx=5)
        ttk.Button(ph_btn_frame, text="Импорт из JSON", command=self.import_placeholders).pack(side='left', padx=5)
        ttk.Button(ph_btn_frame, text="Экспорт в JSON", command=self.export_placeholders).pack(side='left', padx=5)

        # Параметры таблиц
        style_label = ttk.Label(tab_set_gen, text="Параметры таблицы:")
        style_label.pack(anchor='w', padx=10, pady=(10, 3))
        style_frm = ttk.Frame(tab_set_gen)
        style_frm.pack(fill='x', padx=10, pady=5)
        self.var_header_bold = tk.BooleanVar(value=settings.data['table_style']['header_bold'])
        ttk.Checkbutton(style_frm, text="Жирный заголовок", variable=self.var_header_bold).grid(row=0, column=0, padx=5, pady=2)
        ttk.Label(style_frm, text="Выравнивание:").grid(row=0, column=1, padx=5, pady=2, sticky='w')
        self.combo_align = ttk.Combobox(style_frm, values=['left', 'center', 'right'], state='readonly', width=10)
        self.combo_align.set(settings.data['table_style']['alignment'])
        self.combo_align.grid(row=0, column=2, padx=5, pady=2)
        ttk.Label(style_frm, text="Шрифт:").grid(row=0, column=3, padx=5, pady=2, sticky='w')
        self.entry_font_name = ttk.Entry(style_frm, width=15)
        self.entry_font_name.insert(0, settings.data['table_style']['font_name'])
        self.entry_font_name.grid(row=0, column=4, padx=5, pady=2)
        ttk.Label(style_frm, text="Размер:").grid(row=0, column=5, padx=5, pady=2, sticky='w')
        self.spin_font_size = ttk.Spinbox(style_frm, from_=6, to=48, width=5)
        self.spin_font_size.delete(0, 'end')
        self.spin_font_size.insert(0, settings.data['table_style']['font_size'])
        self.spin_font_size.grid(row=0, column=6, padx=5, pady=2)

        # Настройки экспорта
        export_label = ttk.Label(tab_set_gen, text="Настройки экспорта:")
        export_label.pack(anchor='w', padx=10, pady=(10, 3))
        export_frm = ttk.Frame(tab_set_gen)
        export_frm.pack(fill='x', padx=10, pady=5)
        ttk.Label(export_frm, text="Формат файла:").pack(side='left', padx=5)
        self.combo_export_format = ttk.Combobox(export_frm, values=['docx', 'pdf'], state='readonly', width=10)
        self.combo_export_format.set(settings.data['export']['format'])
        self.combo_export_format.pack(side='left', padx=5)
        self.var_compress_images = tk.BooleanVar(value=settings.data['export']['compress_images'])
        ttk.Checkbutton(export_frm, text="Сжимать изображения", variable=self.var_compress_images).pack(side='left', padx=10)
        ttk.Label(export_frm, text="Пароль для PDF:").pack(side='left', padx=5)
        self.entry_pdf_password = ttk.Entry(export_frm, width=20, show='*')
        self.entry_pdf_password.insert(0, settings.data['export']['pdf_password'])
        self.entry_pdf_password.pack(side='left', padx=5)

        ttk.Button(tab_set_gen, text="Сохранить настройки в профиль", command=self.save_profile).pack(padx=10, pady=10, anchor='e')
        ttk.Button(tab_set_gen, text="Предпросмотр итогового документа", command=self.preview_generated_document).pack(padx=10, pady=5, anchor='e')
        ttk.Button(tab_set_gen, text="Генерировать документ", command=self.generate_document).pack(padx=10, pady=5, anchor='e')

        # Вкладка Лог
        tab_log = ttk.Frame(self.tabs)
        self.tabs.add(tab_log, text="Лог")
        self.txt_log = tk.Text(tab_log, state='disabled', wrap='word')
        self.txt_log.pack(fill='both', expand=True, padx=5, pady=5)
        ttk.Button(tab_log, text="Экспорт лога", command=self.export_log).pack(pady=5)

    def create_menu(self):
        menubar = tk.Menu(self)
        file_menu = tk.Menu(menubar, tearoff=0)
        file_menu.add_command(label="Открыть Excel (Ctrl+O)", command=self.load_excel_dialog)
        file_menu.add_command(label="Генерировать документ (Ctrl+S)", command=self.generate_document)
        file_menu.add_separator()
        file_menu.add_command(label="Выход", command=self.quit)
        menubar.add_cascade(label="Файл", menu=file_menu)
        self.config(menu=menubar)

    def bind_shortcuts(self):
        self.bind("<Control-o>", lambda e:self.load_excel_dialog())
        self.bind("<Control-s>", lambda e:self.generate_document())

    def on_profile_change(self, event=None):
        selected = self.combo_profiles.get()
        if selected:
            self.selected_profile = selected
            if settings.load_profile(selected):
                self.load_settings_to_gui()
                messagebox.showinfo("Профиль", f"Профиль '{selected}' загружен")
            else:
                messagebox.showerror("Ошибка", f"Ошибка загрузки профиля: {selected}")

    def load_settings_to_gui(self):
        d = settings.data
        ex = d.get("export", {})
        ts = d.get("table_style", {})

        if hasattr(self, "combo_export_format"):
            self.combo_export_format.set(ex.get("format", "docx"))
        if hasattr(self, "var_compress_images"):
            self.var_compress_images.set(ex.get("compress_images", True))
        if hasattr(self, "entry_pdf_password"):
            self.entry_pdf_password.delete(0, "end")
            self.entry_pdf_password.insert(0, ex.get("pdf_password", ""))

        if hasattr(self, "var_header_bold"):
            self.var_header_bold.set(ts.get("header_bold", True))
        if hasattr(self, "combo_align"):
            self.combo_align.set(ts.get("alignment", "center"))
        if hasattr(self, "entry_font_name"):
            self.entry_font_name.delete(0, "end")
            self.entry_font_name.insert(0, ts.get("font_name", "Calibri"))
        if hasattr(self, "spin_font_size"):
            self.spin_font_size.delete(0, "end")
            self.spin_font_size.insert(0, ts.get("font_size", 11))

        # Set template
        if hasattr(self, "combo_templates"):
            self.load_templates()
            tmpl = ex.get("template")
            if tmpl in self.templates:
                self.combo_templates.set(tmpl)
                self.current_template = tmpl
            elif self.templates:
                self.combo_templates.set(self.templates[0])
                self.current_template = self.templates[0]

    def save_gui_to_settings(self):
        d = settings.data
        exp = d.setdefault("export", {})
        exp["format"] = self.combo_export_format.get() if hasattr(self, "combo_export_format") else "docx"
        exp["compress_images"] = self.var_compress_images.get() if hasattr(self, "var_compress_images") else True
        exp["pdf_password"] = self.entry_pdf_password.get() if hasattr(self, "entry_pdf_password") else ""
        exp["template"] = self.combo_templates.get() if hasattr(self, "combo_templates") else None

        ts = d.setdefault("table_style", {})
        ts["header_bold"] = self.var_header_bold.get() if hasattr(self, "var_header_bold") else True
        ts["alignment"] = self.combo_align.get() if hasattr(self, "combo_align") else "center"
        ts["font_name"] = self.entry_font_name.get() if hasattr(self, "entry_font_name") else "Calibri"
        try:
            ts["font_size"] = int(self.spin_font_size.get()) if hasattr(self, "spin_font_size") else 11
        except:
            ts["font_size"] = 11

        settings.save()

    def load_templates(self):
        self.templates = sorted([f for f in os.listdir(TEMPLATES_DIR) if f.lower().endswith(".docx")])
        if hasattr(self, "combo_templates"):
            self.combo_templates["values"] = self.templates
            if self.templates and self.current_template not in self.templates:
                self.combo_templates.set(self.templates[0])
                self.current_template = self.templates[0]

    # Event handlers
    def load_excel_dialog(self):
        path = filedialog.askopenfilename(title="Выберите Excel файл", filetypes=[("Excel Files", "*.xlsx *.xls")])
        if not path:
            return
        try:
            self.grouped_data = load_and_transform_excel_grouped(path)
            rows = []
            for art, info in self.grouped_data.items():
                obj = info["object_full"]
                ptype = info["product_type"]
                for section in info["sections"]:
                    if section["header"]:
                        rows.append(
                            {
                                "Объект": obj,
                                "Тип товара": ptype,
                                "Артикул": art,
                                "Параметр": f"Раздел: {section['header']}",
                                "Значение": "",
                            }
                        )
                    for param in section["params"]:
                        rows.append(
                            {
                                "Объект": obj,
                                "Тип товара": ptype,
                                "Артикул": art,
                                "Параметр": param["parameter"],
                                "Значение": param["value"],
                            }
                        )
            self.filtered_df = pd.DataFrame(rows)
            self.filtered_df_backup = self.filtered_df.copy()  # for reset filter
            self.update_data_table()
            self.combo_filter_col["values"] = list(self.filtered_df.columns)
            if not self.combo_filter_col.get():
                self.combo_filter_col.set(self.filtered_df.columns[0])
            self.basket_df = pd.DataFrame(columns=self.filtered_df.columns)
            self.update_basket_table()
            messagebox.showinfo("Успех", f"Данные загружены: {len(self.filtered_df)} записей")
            logger.log(f"Файл загружен: {path}, записей: {len(self.filtered_df)}")
        except Exception as e:
            traceback.print_exc()
            messagebox.showerror("Ошибка загрузки", str(e))
            logger.log(f"Ошибка загрузки: {e}")

    def update_data_table(self):
        self.tree_data.delete(*self.tree_data.get_children())
        if self.filtered_df.empty:
            self.tree_data["columns"] = []
            return
        self.tree_data["columns"] = list(self.filtered_df.columns)
        for col in self.filtered_df.columns:
            self.tree_data.heading(col, text=col)
            self.tree_data.column(col, width=160, anchor="w")
        for _, row in self.filtered_df.iterrows():
            self.tree_data.insert("", "end", values=list(row))

    def quick_search(self, event=None):
        if self.filtered_df_backup.empty:
            return
        query = self.var_quick_search.get().lower().strip()
        if not query:
            self.filtered_df = self.filtered_df_backup.copy()
        else:
            self.filtered_df = self.filtered_df_backup[
                self.filtered_df_backup['Артикул'].str.lower().str.contains(query, na=False)
            ]
        self.update_data_table()

    def advanced_filter(self, event=None):
        if self.filtered_df_backup.empty:
            return
        col = self.combo_filter_col.get()
        op = self.combo_filter_op.get()
        val = self.var_filter_val.get().lower().strip()
        if not col or not val:
            self.filtered_df = self.filtered_df_backup.copy()
            self.update_data_table()
            return
        series = self.filtered_df_backup[col].astype(str).str.lower()
        mask = None
        if op == "содержит":
            mask = series.str.contains(val, na=False)
        elif op == "равно":
            mask = series == val
        elif op == "начинается с":
            mask = series.str.startswith(val, na=False)
        elif op == "оканчивается на":
            mask = series.str.endswith(val, na=False)
        if mask is not None:
            self.filtered_df = self.filtered_df_backup[mask]
            self.update_data_table()

    def clear_filter(self):
        self.var_quick_search.set("")
        self.var_filter_val.set("")
        if hasattr(self, 'filtered_df_backup'):
            self.filtered_df = self.filtered_df_backup.copy()
            self.update_data_table()

    def add_selected_to_basket(self, event=None):
        sel = self.tree_data.selection()
        if not sel:
            messagebox.showinfo("Добавление", "Выделите записи для добавления в корзину")
            return
        rows = [self.tree_data.item(iid)["values"] for iid in sel]
        df_add = pd.DataFrame(rows, columns=self.filtered_df.columns)
        self.basket_df = pd.concat([self.basket_df, df_add], ignore_index=True)
        self.update_basket_table()
        logger.log(f"В корзину добавлено: {len(rows)} записей")

    def update_basket_table(self):
        self.tree_basket.delete(*self.tree_basket.get_children())
        if self.basket_df.empty:
            self.tree_basket["columns"] = []
            return
        self.tree_basket["columns"] = list(self.basket_df.columns)
        for col in self.basket_df.columns:
            self.tree_basket.heading(col, text=col)
            self.tree_basket.column(col, width=160, anchor="w")
        for _, row in self.basket_df.iterrows():
            self.tree_basket.insert("", "end", values=list(row))

    def remove_selected_from_basket(self):
        sel = self.tree_basket.selection()
        if not sel:
            messagebox.showinfo("Удаление", "Выделите записи для удаления из корзины")
            return
        idxs = [self.tree_basket.index(iid) for iid in sel]
        self.basket_df.drop(self.basket_df.index[idxs], inplace=True)
        self.basket_df.reset_index(drop=True, inplace=True)
        self.update_basket_table()
        logger.log(f"Удалено из корзины: {len(idxs)} записей")

    def clear_basket(self):
        if messagebox.askyesno("Очистка корзины", "Очистить корзину?"):
            self.basket_df = pd.DataFrame(columns=self.filtered_df.columns)
            self.update_basket_table()
            logger.log("Корзина очищена")

    def show_basket_text(self):
        self.txt_basket_text.delete("1.0", "end")
        if self.basket_df.empty:
            self.txt_basket_text.insert("end", "(Корзина пуста)")
        else:
            self.txt_basket_text.insert("end", self.basket_df.to_string(index=False))

    def context_menu_data(self, event):
        iid = self.tree_data.identify_row(event.y)
        if not iid:
            return
        menu = tk.Menu(self, tearoff=0)
        menu.add_command(label="Добавить в корзину", command=self.add_selected_to_basket)
        menu.post(event.x_root, event.y_root)

    def context_menu_basket(self, event):
        iid = self.tree_basket.identify_row(event.y)
        if not iid:
            return
        menu = tk.Menu(self, tearoff=0)
        menu.add_command(label="Удалить из корзины", command=self.remove_selected_from_basket)
        menu.post(event.x_root, event.y_root)

    def preview_template(self):
        if not self.current_template:
            messagebox.showwarning("Предпросмотр", "Шаблон не выбран")
            return
        path = os.path.join(TEMPLATES_DIR, self.current_template)
        try:
            doc = Document(path)
            images = []
            for rel in doc.part._rels:
                target = doc.part._rels[rel].target_ref
                if target.lower().endswith((".png", ".jpg", ".jpeg", ".bmp", ".gif")):
                    images.append(doc.part._rels[rel]._target.part.blob)
                    break
            if images:
                img = Image.open(BytesIO(images[0]))
                img_tk = pil_image_to_tk(img)
            else:
                img = Image.new("RGB", (400, 250), "white")
                from PIL import ImageDraw, ImageFont

                d = ImageDraw.Draw(img)
                fnt = ImageFont.load_default()
                d.text((10, 120), f"Предпросмотр\n{self.current_template}", fill="black", font=fnt)
                img_tk = pil_image_to_tk(img)
            win = tk.Toplevel(self)
            win.title(f"Предпросмотр '{self.current_template}'")
            label = ttk.Label(win, image=img_tk)
            label.image = img_tk  # сохранить ссылку
            label.pack()
            win.transient(self)
            win.grab_set()
            win.focus_set()
        except Exception as e:
            messagebox.showerror("Ошибка", f"Ошибка предпросмотра шаблона:\n{e}")

    def preview_generated_document(self):
        if self.basket_df.empty:
            messagebox.showwarning("Предпросмотр", "Корзина пуста")
            return
        if not self.current_template:
            messagebox.showwarning("Предпросмотр", "Шаблон не выбран")
            return
        try:
            with tempfile.NamedTemporaryFile(delete=False, suffix=".docx") as tmpf:
                tmpf.close()
                self.generate_document(path=tmpf.name, show_messages=False)
                if os.name == 'nt':
                    os.startfile(tmpf.name)
                else:
                    try:
                        subprocess.Popen(["open", tmpf.name])  # macOS
                    except:
                        subprocess.Popen(["xdg-open", tmpf.name])  # Linux
        except Exception as e:
            messagebox.showerror("Ошибка предпросмотра", str(e))

    def save_profile(self):
        try:
            self.save_gui_to_settings()
            if not self.selected_profile:
                prof_name = simpledialog.askstring("Сохранить профиль", "Имя профиля (без расширения):", parent=self)
                if not prof_name:
                    messagebox.showinfo("Отмена", "Сохранение отменено")
                    return
                fname = prof_name.strip() + ".json"
            else:
                fname = self.selected_profile
            if settings.save_profile(fname):
                self.selected_profile = fname
                self.load_profiles()
                self.combo_profiles.set(fname)
                messagebox.showinfo("Успех", f"Профиль {fname} сохранён")
        except Exception as e:
            messagebox.showerror("Ошибка", f"Ошибка сохранения профиля:\n{e}")

    def load_profile(self):
        prof = self.combo_profiles.get()
        if not prof:
            return
        if settings.load_profile(prof):
            self.selected_profile = prof
            self.load_settings_to_gui()
            messagebox.showinfo("Успех", f"Профиль {prof} загружен")

    def load_profiles(self):
        if not os.path.exists(PROFILES_DIR):
            os.makedirs(PROFILES_DIR)
        files = sorted([f for f in os.listdir(PROFILES_DIR) if f.endswith('.json')])
        self.combo_profiles['values'] = files
        if files and not self.selected_profile:
            self.combo_profiles.set(files[0])
            self.selected_profile = files[0]

    def load_settings_to_gui(self):
        d = settings.data
        ex = d.get('export', {})
        ts = d.get('table_style', {})

        if hasattr(self, 'combo_export_format'):
            self.combo_export_format.set(ex.get('format', 'docx'))
        if hasattr(self, 'var_compress_images'):
            self.var_compress_images.set(ex.get('compress_images', True))
        if hasattr(self, 'entry_pdf_password'):
            self.entry_pdf_password.delete(0, 'end')
            self.entry_pdf_password.insert(0, ex.get('pdf_password',''))

        if hasattr(self, 'var_header_bold'):
            self.var_header_bold.set(ts.get('header_bold', True))
        if hasattr(self, 'combo_align'):
            self.combo_align.set(ts.get('alignment', 'center'))
        if hasattr(self, 'entry_font_name'):
            self.entry_font_name.delete(0, 'end')
            self.entry_font_name.insert(0, ts.get('font_name', 'Calibri'))
        if hasattr(self, 'spin_font_size'):
            self.spin_font_size.delete(0, 'end')
            self.spin_font_size.insert(0, ts.get('font_size', 11))

        # Установка шаблона
        if hasattr(self, 'combo_templates'):
            self.load_templates()
            tmpl = ex.get('template')
            if self.templates:
                if tmpl in self.templates:
                    self.combo_templates.set(tmpl)
                    self.current_template = tmpl
                else:
                    self.combo_templates.set(self.templates[0])
                    self.current_template = self.templates[0]

    def save_gui_to_settings(self):
        d = settings.data
        exp = d.setdefault('export', {})
        exp['format'] = self.combo_export_format.get() if hasattr(self, 'combo_export_format') else 'docx'
        exp['compress_images'] = self.var_compress_images.get() if hasattr(self, 'var_compress_images') else True
        exp['pdf_password'] = self.entry_pdf_password.get() if hasattr(self, 'entry_pdf_password') else ''
        exp['template'] = self.combo_templates.get() if hasattr(self, 'combo_templates') else None

        ts = d.setdefault('table_style', {})
        ts['header_bold'] = self.var_header_bold.get() if hasattr(self, 'var_header_bold') else True
        ts['alignment'] = self.combo_align.get() if hasattr(self, 'combo_align') else 'center'
        ts['font_name'] = self.entry_font_name.get() if hasattr(self, 'entry_font_name') else 'Calibri'
        try:
            ts['font_size'] = int(self.spin_font_size.get()) if hasattr(self, 'spin_font_size') else 11
        except:
            ts['font_size'] = 11

        settings.save()

def main():
    app = App()
    app.mainloop()

if __name__ == '__main__':
    main()