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

import pandas as pd
from docx import Document
from docx.shared import Pt, RGBColor
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.oxml.ns import qn
from docx.oxml import OxmlElement, parse_xml

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")
CONFIG_DIR = os.path.join(WORKDIR, "config")
PROFILES_DIR = os.path.join(CONFIG_DIR, "profiles")

for d in (TEMPLATES_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 = ['Объект', 'Свойство', 'Значение']

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)
        if hasattr(app_ref, "txt_log") and app_ref.txt_log:
            app_ref.txt_log.config(state='normal')
            app_ref.txt_log.insert("end", record + "\n")
            app_ref.txt_log.see("end")
            app_ref.txt_log.config(state='disabled')

    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()
app_ref = None  # Ссылка на приложение, для логирования в UI

class Settings:
    def __init__(self):
        self.data = {
            "table_style": {
                "header_bold": True,
                "alignment": "center",
                "font_name": "Calibri",
                "font_size": 11,
                "border_style": "тонкая",
                # Новые настройки для заголовка раздела таблицы:
                "section_header_font_name": "Calibri",
                "section_header_font_size": 12,
                "section_header_bold": True,
                "section_header_align": "center",
                "section_header_bg_color": "FFFF00"  # Желтый фон по умолчанию
            },
            "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

                ts = self.data.get('table_style', {})
                defaults = {
                    "border_style": "тонкая",
                    "section_header_font_name": "Calibri",
                    "section_header_font_size": 12,
                    "section_header_bold": True,
                    "section_header_align": "center",
                    "section_header_bg_color": "FFFF00"
                }
                for k, v in defaults.items():
                    if k not in ts:
                        ts[k] = v
                self.data['table_style'] = ts

                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)

            ts = self.data.get('table_style', {})
            defaults = {
                "border_style": "тонкая",
                "section_header_font_name": "Calibri",
                "section_header_font_size": 12,
                "section_header_bold": True,
                "section_header_align": "center",
                "section_header_bg_color": "FFFF00"
            }
            for k, v in defaults.items():
                if k not in ts:
                    ts[k] = v
            self.data['table_style'] = ts

            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 is not None 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 is not None 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 inline_replace_in_paragraph(paragraph, placeholder, replacement):
    runs = paragraph.runs
    full_text = "".join(run.text for run in runs)
    if placeholder not in full_text:
        return
    new_text = full_text.replace(placeholder, replacement)
    for run in runs:
        run.text = ""
    if runs:
        runs[0].text = new_text

def docx_replace_placeholder(doc, key, value):
    ph = "{{" + key + "}}"
    # Заменяем в параграфах
    for p in doc.paragraphs:
        if ph in p.text:
            inline_replace_in_paragraph(p, 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:
                        inline_replace_in_paragraph(p, ph, str(value))

    # Заменяем в колонтитулах
    replace_placeholder_in_header_footer(doc, key, value)

def replace_placeholder_in_header_footer(doc, key, value):
    ph = "{{" + key + "}}"
    for section in doc.sections:
        header = section.header
        for p in header.paragraphs:
            if ph in p.text:
                inline_replace_in_paragraph(p, ph, str(value))
        footer = section.footer
        for p in footer.paragraphs:
            if ph in p.text:
                inline_replace_in_paragraph(p, ph, str(value))

def set_table_border(table, border_style):
    if border_style == 'нет':
        border_xml = """
        <w:tblBorders %s>
            <w:top w:val="nil"/>
            <w:left w:val="nil"/>
            <w:bottom w:val="nil"/>
            <w:right w:val="nil"/>
            <w:insideH w:val="nil"/>
            <w:insideV w:val="nil"/>
        </w:tblBorders>
        """ % nsdecls('w')
    elif border_style == 'тонкая':
        border_xml = """
        <w:tblBorders %s>
            <w:top w:val="single" w:sz="4" w:space="0" w:color="000000"/>
            <w:left w:val="single" w:sz="4" w:space="0" w:color="000000"/>
            <w:bottom w:val="single" w:sz="4" w:space="0" w:color="000000"/>
            <w:right w:val="single" w:sz="4" w:space="0" w:color="000000"/>
            <w:insideH w:val="single" w:sz="4" w:space="0" w:color="000000"/>
            <w:insideV w:val="single" w:sz="4" w:space="0" w:color="000000"/>
        </w:tblBorders>
        """ % nsdecls('w')
    elif border_style == 'толстая':
        border_xml = """
        <w:tblBorders %s>
            <w:top w:val="single" w:sz="12" w:space="0" w:color="000000"/>
            <w:left w:val="single" w:sz="12" w:space="0" w:color="000000"/>
            <w:bottom w:val="single" w:sz="12" w:space="0" w:color="000000"/>
            <w:right w:val="single" w:sz="12" w:space="0" w:color="000000"/>
            <w:insideH w:val="single" w:sz="12" w:space="0" w:color="000000"/>
            <w:insideV w:val="single" w:sz="12" w:space="0" w:color="000000"/>
        </w:tblBorders>
        """ % nsdecls('w')
    else:
        border_xml = """
        <w:tblBorders %s>
            <w:top w:val="single" w:sz="4" w:space="0" w:color="000000"/>
            <w:left w:val="single" w:sz="4" w:space="0" w:color="000000"/>
            <w:bottom w:val="single" w:sz="4" w:space="0" w:color="000000"/>
            <w:right w:val="single" w:sz="4" w:space="0" w:color="000000"/>
            <w:insideH w:val="single" w:sz="4" w:space="0" w:color="000000"/>
            <w:insideV w:val="single" w:sz="4" w:space="0" w:color="000000"/>
        </w:tblBorders>
        """ % nsdecls('w')

    tblPr = table._element.tblPr
    for child in tblPr.findall("{http://schemas.openxmlformats.org/wordprocessingml/2006/main}tblBorders"):
        tblPr.remove(child)
    borders = parse_xml(border_xml)
    tblPr.append(borders)

def set_cell_background(cell, hex_color):
    tc = cell._tc
    tcPr = tc.get_or_add_tcPr()
    shd = OxmlElement('w:shd')
    shd.set(qn('w:fill'), hex_color)
    tcPr.append(shd)

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

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

        self.grouped_data = {}
        self.filtered_df = pd.DataFrame()
        self.filtered_df_backup = 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()

        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="Данные")

        # Left Frame Data Tab
        left_frame = ttk.Frame(tab_data)
        left_frame.pack(side="left", fill="both", expand=True, padx=5, pady=5)

        # Search Frame
        fsearch = ttk.Frame(left_frame)
        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)

        # Filter Frame
        ffilter = ttk.Frame(left_frame)
        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)

        # Tree data with scrollbar
        tbl_frame = ttk.Frame(left_frame)
        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-1>", self.show_item_params)
        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(left_frame)
        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)

        # Right frame for parameters
        right_frame = ttk.Frame(tab_data)
        right_frame.pack(side="right", fill="both", expand=True, padx=5, pady=5)

        self.tree_params = ttk.Treeview(right_frame, show="headings", columns=["Параметр", "Значение"])
        self.tree_params.pack(fill="both", expand=True)
        self.tree_params.heading("Параметр", text="Параметр")
        self.tree_params.heading("Значение", text="Значение")
        self.tree_params.column("Параметр", width=150, anchor="w")
        self.tree_params.column("Значение", width=150, anchor="w")

        # Basket frame bottom
        basket_frame = ttk.Frame(self)
        basket_frame.pack(side="bottom", fill="x", 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(basket_frame)
        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(basket_frame, height=10)
        self.txt_basket_text.pack(fill="both", padx=5, pady=5)

        # Tab Settings and Generation
        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)

        # Table parameters frame
        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)

        # Table style options
        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)

        ttk.Label(style_frm, text="Стиль рамки:").grid(row=0, column=7, padx=5, pady=2, sticky='w')
        self.combo_border_style = ttk.Combobox(style_frm, values=['нет', 'тонкая', 'толстая'], state='readonly', width=10)
        self.combo_border_style.set(settings.data['table_style'].get('border_style', 'тонкая'))
        self.combo_border_style.grid(row=0, column=8, padx=5, pady=2)

        # Section header format
        ttk.Label(style_frm, text="Шрифт заголовка раздела:").grid(row=1, column=0, sticky='w', padx=5, pady=3)
        self.entry_sec_header_font_name = ttk.Entry(style_frm, width=15)
        self.entry_sec_header_font_name.grid(row=1, column=1, padx=5)
        self.entry_sec_header_font_name.insert(0, settings.data['table_style'].get("section_header_font_name", "Calibri"))

        ttk.Label(style_frm, text="Размер шрифта:").grid(row=1, column=2, sticky='w', padx=5, pady=3)
        self.spin_sec_header_font_size = ttk.Spinbox(style_frm, from_=6, to=48, width=5)
        self.spin_sec_header_font_size.grid(row=1, column=3, padx=5)
        self.spin_sec_header_font_size.delete(0, "end")
        self.spin_sec_header_font_size.insert(0, settings.data['table_style'].get("section_header_font_size", 12))

        self.var_sec_header_bold = tk.BooleanVar(value=settings.data['table_style'].get("section_header_bold", True))
        ttk.Checkbutton(style_frm, text="Жирный заголовок раздела", variable=self.var_sec_header_bold).grid(row=1, column=4, padx=5)

        ttk.Label(style_frm, text="Выравнивание заголовка:").grid(row=1, column=5, sticky='w', padx=5, pady=3)
        self.combo_sec_header_align = ttk.Combobox(style_frm, values=['left', 'center', 'right'], state='readonly', width=10)
        self.combo_sec_header_align.grid(row=1, column=6, padx=5)
        self.combo_sec_header_align.set(settings.data['table_style'].get("section_header_align", "center"))

        ttk.Label(style_frm, text="Цвет фона заголовка (HEX, 6 символов):").grid(row=1, column=7, sticky='w', padx=5, pady=3)
        self.entry_sec_header_bg_color = ttk.Entry(style_frm, width=10)
        self.entry_sec_header_bg_color.grid(row=1, column=8, padx=5)
        self.entry_sec_header_bg_color.insert(0, settings.data['table_style'].get("section_header_bg_color", "FFFF00"))

        # Export settings
        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')

        # Log tab
        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 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"]
                rows.append({"Объект": obj, "Тип товара": ptype, "Артикул": art})
            self.filtered_df = pd.DataFrame(rows)
            self.filtered_df_backup = self.filtered_df.copy()
            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.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 show_item_params(self, event):
        sel = self.tree_data.selection()
        if not sel:
            return
        iid = sel[0]
        item = self.tree_data.item(iid)
        art = item["values"][2]
        if art in self.grouped_data:
            params = self.grouped_data[art]["sections"][0]["params"]
            self.tree_params.delete(*self.tree_params.get_children())
            for param in params:
                self.tree_params.insert("", "end", values=(param["parameter"], param["value"]))

    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.basket_df = pd.concat([self.basket_df, df_add], ignore_index=True)
        self.basket_df.drop_duplicates(inplace=True)
        self.basket_df.reset_index(drop=True, inplace=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.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:
            if os.name == 'nt':
                os.startfile(path)
            else:
                try:
                    subprocess.Popen(["open", path])
                except:
                    subprocess.Popen(["xdg-open", path])
        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])
                    except:
                        subprocess.Popen(["xdg-open", tmpf.name])
        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 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 on_template_change(self, event=None):
        selected = self.combo_templates.get()
        if selected:
            self.current_template = selected
            logger.log(f"Выбран шаблон: {selected}")

    def load_placeholders_to_tree(self):
        self.user_placeholders = placeholders.placeholders.copy()
        self.refresh_placeholders_view()

    def refresh_placeholders_view(self):
        self.tree_placeholders.delete(*self.tree_placeholders.get_children())
        for k, v in self.user_placeholders.items():
            self.tree_placeholders.insert("", "end", values=(k, v))

    def add_placeholder(self):
        name = simpledialog.askstring("Добавить плейсхолдер", "Имя плейсхолдера:", parent=self)
        if not name:
            return
        if name in self.user_placeholders:
            messagebox.showerror("Ошибка", "Такой плейсхолдер уже существует")
            return
        value = simpledialog.askstring("Добавить плейсхолдер", "Значение плейсхолдера:", parent=self)
        if value is None:
            return
        self.user_placeholders[name] = value
        placeholders.placeholders = self.user_placeholders.copy()
        placeholders.save()
        self.refresh_placeholders_view()
        logger.log(f"Добавлен плейсхолдер: {name} = {value}")

    def edit_placeholder(self):
        sel = self.tree_placeholders.selection()
        if not sel:
            messagebox.showinfo("Редактировать", "Выделите плейсхолдер")
            return
        iid = sel[0]
        name, old_value = self.tree_placeholders.item(iid, "values")
        new_value = simpledialog.askstring("Редактировать плейсхолдер", f"Значение плейсхолдера '{name}':", initialvalue=old_value, parent=self)
        if new_value is None:
            return
        self.user_placeholders[name] = new_value
        placeholders.placeholders = self.user_placeholders.copy()
        placeholders.save()
        self.refresh_placeholders_view()
        logger.log(f"Изменён плейсхолдер: {name} = {new_value}")

    def delete_placeholder(self):
        sel = self.tree_placeholders.selection()
        if not sel:
            messagebox.showinfo("Удалить", "Выделите плейсхолдер")
            return
        iid = sel[0]
        name = self.tree_placeholders.item(iid, "values")[0]
        if messagebox.askyesno("Удалить плейсхолдер", f"Удалить плейсхолдер '{name}'?"):
            self.user_placeholders.pop(name, None)
            placeholders.placeholders = self.user_placeholders.copy()
            placeholders.save()
            self.refresh_placeholders_view()
            logger.log(f"Удалён плейсхолдер: {name}")

    def import_placeholders(self):
        path = filedialog.askopenfilename(title="Импорт плейсхолдеров из JSON", filetypes=[("JSON", "*.json")])
        if not path:
            return
        try:
            with open(path, 'r', encoding='utf-8') as f:
                d = json.load(f)
            if not isinstance(d, dict):
                raise ValueError("Файл должен содержать словарь плейсхолдеров")
            self.user_placeholders.update(d)
            placeholders.placeholders = self.user_placeholders.copy()
            placeholders.save()
            self.refresh_placeholders_view()
            messagebox.showinfo("Импорт", f"Импортировано {len(d)} плейсхолдеров")
            logger.log(f"Импортировано плейсхолдеров: {len(d)}")
        except Exception as e:
            messagebox.showerror("Ошибка импорта", str(e))
            logger.log(f"Ошибка импорта плейсхолдеров: {e}")

    def export_placeholders(self):
        path = filedialog.asksaveasfilename(title="Экспорт плейсхолдеров в JSON", defaultextension=".json",
                                            filetypes=[("JSON", "*.json")])
        if not path:
            return
        try:
            with open(path, 'w', encoding='utf-8') as f:
                json.dump(self.user_placeholders, f, ensure_ascii=False, indent=4)
            messagebox.showinfo("Экспорт", f"Плейсхолдеры экспортированы: {path}")
            logger.log(f"Плейсхолдеры экспортированы: {path}")
        except Exception as e:
            messagebox.showerror("Ошибка экспорта", str(e))
            logger.log(f"Ошибка экспорта плейсхолдеров: {e}")

    def export_log(self):
        path = filedialog.asksaveasfilename(title="Сохранить лог", defaultextension=".txt",
                                            filetypes=[("Text files", "*.txt")])
        if not path:
            return
        logger.save_to_file(path)
        messagebox.showinfo("Лог сохранён", f"Лог сохранён в файл:\n{path}")

    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

        ts["border_style"] = self.combo_border_style.get() if hasattr(self, "combo_border_style") else "тонкая"

        ts["section_header_font_name"] = self.entry_sec_header_font_name.get() if hasattr(self, "entry_sec_header_font_name") else "Calibri"
        try:
            ts["section_header_font_size"] = int(self.spin_sec_header_font_size.get()) if hasattr(self, "spin_sec_header_font_size") else 12
        except:
            ts["section_header_font_size"] = 12
        ts["section_header_bold"] = self.var_sec_header_bold.get() if hasattr(self, "var_sec_header_bold") else True
        ts["section_header_align"] = self.combo_sec_header_align.get() if hasattr(self, "combo_sec_header_align") else "center"
        ts["section_header_bg_color"] = self.entry_sec_header_bg_color.get() if hasattr(self, "entry_sec_header_bg_color") else "FFFF00"

        settings.save()

    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_border_style"):
            self.combo_border_style.set(ts.get("border_style", "тонкая"))

        if hasattr(self, "entry_sec_header_font_name"):
            self.entry_sec_header_font_name.delete(0, "end")
            self.entry_sec_header_font_name.insert(0, ts.get("section_header_font_name", "Calibri"))
        if hasattr(self, "spin_sec_header_font_size"):
            self.spin_sec_header_font_size.delete(0, "end")
            self.spin_sec_header_font_size.insert(0, ts.get("section_header_font_size", 12))
        if hasattr(self, "var_sec_header_bold"):
            self.var_sec_header_bold.set(ts.get("section_header_bold", True))
        if hasattr(self, "combo_sec_header_align"):
            self.combo_sec_header_align.set(ts.get("section_header_align", "center"))
        if hasattr(self, "entry_sec_header_bg_color"):
            self.entry_sec_header_bg_color.delete(0, "end")
            self.entry_sec_header_bg_color.insert(0, ts.get("section_header_bg_color", "FFFF00"))

        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 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:
                if self.current_template not in self.templates:
                    self.combo_templates.set(self.templates[0])
                    self.current_template = self.templates[0]

    def generate_document(self, path=None, show_messages=True):
        if self.basket_df.empty:
            if show_messages:
                messagebox.showwarning("Генерация", "Корзина пуста")
            return
        if not self.current_template:
            if show_messages:
                messagebox.showwarning("Генерация", "Шаблон не выбран")
            return

        self.save_gui_to_settings()

        if not path:
            path = filedialog.asksaveasfilename(title="Сохранить документ",
                                                defaultextension=".docx",
                                                filetypes=[("Word Document", "*.docx")])
            if not path:
                return

        try:
            template_path = os.path.join(TEMPLATES_DIR, self.current_template)
            doc = Document(template_path)

            phs = self.user_placeholders.copy()

            unique_arts = list(dict.fromkeys(self.basket_df["Артикул"]))

            art_info = {}
            art_sections = {}

            for art in unique_arts:
                if art in self.grouped_data:
                    info = self.grouped_data[art]
                    art_info[art] = {
                        "Объект": info["object_full"],
                        "Тип товара": info["product_type"],
                        "Артикул": art,
                    }
                    art_sections[art] = info["sections"]
                else:
                    art_info[art] = {"Объект": "", "Тип товара": "", "Артикул": art}
                    art_sections[art] = []

            if unique_arts:
                first_art = unique_arts[0]
                for key in ["Тип товара", "Артикул", "Объект"]:
                    val = art_info[first_art].get(key, "")
                    phs[key] = val

            for k, v in phs.items():
                docx_replace_placeholder(doc, k, v)

            paras = list(doc.paragraphs)
            placeholder_paragraph = None
            for p in paras:
                if "{{Таблица}}" in p.text:
                    placeholder_paragraph = p
                    break

            if placeholder_paragraph is None:
                logger.log("Плейсхолдер {{Таблица}} не найден в шаблоне")
                if show_messages:
                    messagebox.showwarning("Генерация", "Шаблон не содержит плейсхолдера {{Таблица}}")
                doc.save(path)
                return

            style = settings.data.get("table_style", {})
            border_style = style.get("border_style", "тонкая")

            previous_element = placeholder_paragraph._element

            sh_font_name = style.get("section_header_font_name", "Calibri")
            sh_font_size = style.get("section_header_font_size", 12)
            sh_bold = style.get("section_header_bold", True)
            sh_align = style.get("section_header_align", "center")
            sh_bg_color = style.get("section_header_bg_color", "FFFF00")

            for art in unique_arts:
                for section in art_sections[art]:

                    tbl = doc.add_table(rows=1, cols=2)
                    set_table_border(tbl, border_style)

                    row = tbl.rows[0]
                    cell = row.cells[0]
                    cell.merge(row.cells[1])

                    paragraph = cell.paragraphs[0]
                    paragraph.alignment = {
                        "left": WD_ALIGN_PARAGRAPH.LEFT,
                        "center": WD_ALIGN_PARAGRAPH.CENTER,
                        "right": WD_ALIGN_PARAGRAPH.RIGHT,
                    }.get(sh_align, WD_ALIGN_PARAGRAPH.CENTER)

                    for r in paragraph.runs:
                        r.text = ''

                    run = paragraph.add_run(section.get("header", ""))
                    run.font.name = sh_font_name
                    run.font.size = Pt(sh_font_size)
                    run.bold = sh_bold
                    set_cell_background(cell, sh_bg_color)

                    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_cell = row_cells[i].paragraphs[0]
                            p_cell.style = None  
                            for r in p_cell.runs:
                                r.font.name = style.get("font_name", "Calibri")
                                r.font.size = Pt(style.get("font_size", 11))
                                r.bold = False
                            p_cell.alignment = {
                                "left": WD_ALIGN_PARAGRAPH.LEFT,
                                "right": WD_ALIGN_PARAGRAPH.RIGHT,
                                "center": WD_ALIGN_PARAGRAPH.CENTER,
                            }.get(style.get("alignment", "center"), WD_ALIGN_PARAGRAPH.CENTER)

                    previous_element.addnext(tbl._element)
                    previous_element = tbl._element

            p = placeholder_paragraph._element
            p.getparent().remove(p)

            doc.save(path)
            logger.log(f"Документ сгенерирован и сохранён: {path}")

            if settings.data.get("export", {}).get("format") == "pdf":
                pdf_path = os.path.splitext(path)[0] + ".pdf"
                if WIN32COM_AVAILABLE:
                    convert_docx_to_pdf(path, pdf_path)
                    if show_messages:
                        messagebox.showinfo("Успех", f"Документ сгенерирован и сохранён:\n{path}\nФормат PDF:\n{pdf_path}")
                else:
                    logger.log("Для создания PDF необходим MS Word и pywin32")
                    messagebox.showwarning("PDF", "Конвертация в PDF невозможна. Не найден пакет pywin32.")
            else:
                if show_messages:
                    messagebox.showinfo("Успех", f"Документ сгенерирован и сохранён:\n{path}")

        except Exception as e:
            traceback.print_exc()
            logger.log(f"Ошибка генерации документа: {e}")
            if show_messages:
                messagebox.showerror("Ошибка генерации", str(e))

def convert_docx_to_pdf(docx_path, pdf_path):
    word = win32com.client.Dispatch("Word.Application")
    doc = word.Documents.Open(docx_path)
    doc.SaveAs(pdf_path, FileFormat=17)
    doc.Close()
    word.Quit()

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

if __name__ == '__main__':
    main()

ошибка:
Traceback (most recent call last):
  File "C:\Users\Vshmyglyuk\Desktop\car\car.py", line 1145, in generate_document
    set_table_border(tbl, border_style)
    ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^
  File "C:\Users\Vshmyglyuk\Desktop\car\car.py", line 334, in set_table_border
    """ % nsdecls('w')
          ^^^^^^^
NameError: name 'nsdecls' is not defined
[2025-05-14 11:50:42] Ошибка генерации документа: name 'nsdecls' is not defined
[2025-05-14 12:00:04] Настройки сохранены
Traceback (most recent call last):
  File "C:\Users\Vshmyglyuk\Desktop\car\car.py", line 1145, in generate_document
    set_table_border(tbl, border_style)
    ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^
  File "C:\Users\Vshmyglyuk\Desktop\car\car.py", line 334, in set_table_border
    """ % nsdecls('w')
          ^^^^^^^
NameError: name 'nsdecls' is not defined
[2025-05-14 12:00:04] Ошибка генерации документа: name 'nsdecls' is not defined