import os
import pandas as pd
from PyQt6.QtWidgets import (
    QApplication,
    QMainWindow,
    QVBoxLayout,
    QHBoxLayout,
    QWidget,
    QPushButton,
    QListWidget,
    QLabel,
    QFileDialog,
    QMessageBox,
    QTextEdit,
)
from PyQt6.QtCore import QThread, pyqtSignal
from reportlab.lib.units import mm
from reportlab.pdfgen import canvas
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.lib.utils import ImageReader
import logging

# Настройка логирования
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s'
)

def register_fonts():
    try:
        pdfmetrics.registerFont(TTFont('DejaVuSans', 'DejaVuSans.ttf'))
        pdfmetrics.registerFont(TTFont('DejaVuSans-Bold', 'DejaVuSans-Bold.ttf'))
    except Exception as e:
        logging.error(f"Ошибка при регистрации шрифтов: {e}")
        raise

register_fonts()

# Задаем размер страницы 297 мм x 420 мм (формат A3)
custom_page_size = (297 * mm, 420 * mm)

class PDFGeneratorThread(QThread):
    log_signal = pyqtSignal(str)

    def __init__(self, folder_paths, save_path, photo_files):
        super().__init__()
        self.folder_paths = folder_paths
        self.save_path = save_path
        self.photo_files = photo_files

    def run(self):
        try:
            data = self.aggregate_data()
            if data.empty:
                self.log_signal.emit("Нет данных для обработки.")
                return
            photo_data = self.aggregate_photo_data()
            self.generate_pdfs(data, photo_data)
        except Exception as e:
            self.log_signal.emit(f"Ошибка при выполнении: {e}")

    def aggregate_data(self):
        data = pd.DataFrame()
        for folder in self.folder_paths:
            if not os.path.isdir(folder):
                self.log_signal.emit(f"Предупреждение: {folder} — не существует.")
                continue
            for file in os.listdir(folder):
                if file.endswith(('.xlsx', '.xls')):
                    file_path = os.path.join(folder, file)
                    try:
                        xls = pd.ExcelFile(file_path)
                        for sheet_name in xls.sheet_names:
                            file_data = pd.read_excel(file_path, sheet_name=sheet_name)
                            # Нормализация названий столбцов
                            file_data.columns = file_data.columns.str.strip()
                            if not file_data.empty:
                                self.log_signal.emit(
                                    f"Прочитан файл: {file_path}, лист: {sheet_name}, столбцы: {file_data.columns.tolist()}"
                                )
                                data = pd.concat([data, file_data], ignore_index=True)
                            else:
                                self.log_signal.emit(f"Предупреждение: Лист {sheet_name} в файле {file_path} пустой.")
                    except Exception as e:
                        self.log_signal.emit(f"Ошибка при чтении файла {file_path}: {e}")
        # Нормализация названий столбцов в основном DataFrame
        if not data.empty:
            data.columns = data.columns.str.strip()
        return self.check_columns(data)

    def check_columns(self, data):
        # Преобразование дат в формат datetime, если есть соответствующие столбцы
        if 'Период с' in data.columns:
            data["Период с"] = pd.to_datetime(data["Период с"], dayfirst=True, errors='coerce')
        if 'Период по' in data.columns:
            data["Период по"] = pd.to_datetime(data["Период по"], dayfirst=True, errors='coerce')
        return data

    def aggregate_photo_data(self):
        photo_data = pd.DataFrame()
        for file_path in self.photo_files:
            if not os.path.exists(file_path):
                self.log_signal.emit(f"Предупреждение: Файл {file_path} не существует.")
                continue
            try:
                file_data = pd.read_excel(file_path)
                # Нормализация названий столбцов
                file_data.columns = file_data.columns.str.strip()
                if not file_data.empty:
                    self.log_signal.emit(
                        f"Прочитан файл с фотографиями: {file_path}, столбцы: {file_data.columns.tolist()}"
                    )
                    photo_data = pd.concat([photo_data, file_data], ignore_index=True)
                else:
                    self.log_signal.emit(f"Предупреждение: Файл с фотографиями {file_path} пустой.")
            except Exception as e:
                self.log_signal.emit(f"Ошибка при чтении файла {file_path}: {e}")
        return photo_data

    def generate_pdfs(self, data, photo_data):
        if "Код ТТ" in data.columns:
            data_by_tt = data.groupby("Код ТТ")
        else:
            self.log_signal.emit("Отсутствует столбец 'Код ТТ' в данных. Невозможно сгенерировать PDF.")
            return
        for tt_code, tt_data in data_by_tt:
            if tt_data.empty:
                continue
            if "Название ТТ" in tt_data.columns:
                tt_name_series = tt_data["Название ТТ"].dropna()
                if not tt_name_series.empty:
                    tt_name = tt_name_series.iloc[0]
                else:
                    tt_name = "Unknown TT Name"
            else:
                tt_name = "Unknown TT Name"

            period_start = tt_data["Период с"].min() if "Период с" in tt_data.columns else None
            period_end = tt_data["Период по"].max() if "Период по" in tt_data.columns else None

            period_start_str = period_start.strftime('%Y-%m-%d') if pd.notnull(period_start) else "unknown_start"
            period_end_str = period_end.strftime('%Y-%m-%d') if pd.notnull(period_end) else "unknown_end"

            safe_tt_name = "".join([c if c.isalnum() or c in (' ', '_', '-') else "_" for c in tt_name])
            pdf_name = f"{tt_code}_{safe_tt_name}_{period_start_str}_{period_end_str}.pdf"
            pdf_path = os.path.join(self.save_path, pdf_name)

            c = canvas.Canvas(pdf_path, pagesize=custom_page_size)
            width, height = custom_page_size

            y_position = height - 50

            self.draw_text(c, "Администратору необходимо осуществить выкладку товара на ДМП:", 30, y_position, "DejaVuSans", 10)
            y_position -= 20

            equipment_types = [
                "Магнит-хит", "Оборудование КА", "Навесное оборудование",
                "Оборудование сети", "Желтый акционный стеллаж"
            ]
            for equipment_type in equipment_types:
                if "Тип оборудования" in tt_data.columns:
                    eq_data = tt_data[tt_data["Тип оборудования"] == equipment_type]
                    if not eq_data.empty:
                        y_position = self.start_new_page_if_needed(c, y_position, height)
                        self.draw_text(c, f"Оборудование: {equipment_type}", 30, y_position, "DejaVuSans-Bold", 10)
                        y_position -= 20

                        y_position = self.process_projects(c, eq_data, equipment_type, photo_data, width, height, y_position)
            c.save()
            self.log_signal.emit(f"PDF создан: {pdf_path}")

    def process_projects(self, c, eq_data, equipment_type, photo_data, width, height, y_position):
        left_margin = 30
        right_margin = 30
        column_gap = 10
        max_page_width = width - left_margin - right_margin
        left_column_width = max_page_width * 0.6
        right_column_width = max_page_width * 0.4

        for project, project_data in eq_data.groupby("Наименование проекта"):
            if project_data.empty:
                continue
            # Определяем высоту, необходимую для вывода деталей проекта
            font_size = 8
            detail_lines = self.get_details(equipment_type, project_data)
            detail_height = len(detail_lines) * (font_size + 10)  # приблизительная высота деталей
            project_title_height = font_size + 10  # высота строки "Проект:..."

            # Проверяем, хватит ли места на текущей странице для деталей проекта
            required_height = project_title_height + detail_height + 50  # +50 для запаса
            if y_position - required_height < 100:
                # Места не хватает, переносим весь проект на новую страницу
                c.showPage()
                y_position = height - 50

            # Начинаем с левого столбца
            current_x = left_margin

            project_title_y = y_position  # Запоминаем y_position для верхней части фото

            # Используем новый метод draw_wrapped_text
            max_text_width = left_column_width - 20  # Отнимаем отступы
            self.draw_wrapped_text(c, f"Проект: {project}", current_x, y_position, "DejaVuSans", font_size, max_text_width)
            y_position -= 20

            details = detail_lines
            for detail in details:
                y_position -= 18
                self.draw_wrapped_text(c, detail, current_x + 10, y_position, "DejaVuSans", font_size, max_text_width - 10)
            y_position -= 10  # Отступ после деталей

            # Сохраняем y_position перед выводом таблицы
            y_position_before_table = y_position

            table_columns = self.get_table_columns(equipment_type, project_data)
            if table_columns:
                table_data = project_data[table_columns].drop_duplicates().fillna("-")
                y_position_after_table = self.draw_table(c, table_data, table_columns, left_column_width, y_position, current_x)
            else:
                y_position_after_table = y_position
                self.log_signal.emit(f"Табличные данные отсутствуют или необходимые столбцы отсутствуют для проекта '{project}'.")

            # Теперь добавляем изображения после вывода таблицы
            # y_top остается прежним, y_bottom обновляется до y_position после таблицы
            y_bottom = y_position_after_table

            self.add_photos_to_pdf(
                c, photo_data, project, right_column_width, project_title_y, y_bottom,
                left_margin + left_column_width + column_gap
            )

            y_position = y_position_after_table - 20  # Отступ перед следующим проектом

        return y_position

    def start_new_page_if_needed(self, c, y_position, height):
        if y_position < 100:
            c.showPage()
            y_position = height - 50
        return y_position

    def get_details(self, equipment_type, project_data):
        details = []

        def add_detail(label, column_name):
            if column_name in project_data.columns:
                value_series = project_data[column_name].dropna()
                if not value_series.empty:
                    value = value_series.iloc[0]
                else:
                    value = '-'
                details.append(f"{label}: {value}")
            else:
                details.append(f"{label}: -")

        if equipment_type in ["Магнит-хит", "Оборудование сети"]:
            add_detail("Название оборудования", "Наименование оборудования")
            add_detail("Полный период размещения", "Полный период размещения")
            add_detail("Ссылка на планограмму", "Ссылка на ПГ")
            add_detail("Комментарий", "Комментарий")
        elif equipment_type in ["Навесное оборудование", "Оборудование КА"]:
            add_detail("Зона размещения", "Зона размещения")
            add_detail("Название оборудования", "Наименование оборудования")
            add_detail("Полный период размещения", "Полный период размещения")
            add_detail("Действия ТТ по окончанию размещения", "Действия ТТ по окончанию размещения")
            add_detail("Комментарий", "Комментарий")
        elif equipment_type == "Желтый акционный стеллаж":
            add_detail("Зона размещения", "Зона размещения")
            add_detail("Ссылка на планограмму", "Ссылка на ПГ")
            add_detail("Комментарий", "Комментарий")
        return details

    def get_table_columns(self, equipment_type, project_data):
        base_columns = []
        if equipment_type in ["Магнит-хит", "Оборудование сети", "Желтый акционный стеллаж"]:
            base_columns = ["Код позиции", "Наименование позиции", "Загрузка в шт", "Место выкладки товара (адрес)"]
        elif equipment_type in ["Навесное оборудование", "Оборудование КА"]:
            base_columns = ["Код позиции", "Наименование позиции", "Загрузка в шт"]

        # Проверяем наличие столбцов в данных проекта
        table_columns = [col for col in base_columns if col in project_data.columns]
        missing_columns = [col for col in base_columns if col not in project_data.columns]
        if missing_columns:
            self.log_signal.emit(f"Предупреждение: В проекте отсутствуют столбцы {missing_columns}.")
        return table_columns

    def draw_table(self, c, table_data, table_columns, max_table_width, y_position, x_start):
        if table_data.empty:
            self.log_signal.emit(f"Табличные данные отсутствуют.")
            return y_position

        self.draw_text(c, "Товарные позиции:", x_start + 10, y_position, "DejaVuSans", 8)
        y_position -= 20

        font_size = 8  # Размер шрифта 8
        col_widths = [self.calculate_column_width(table_data[col], col, font_size=font_size) for col in table_columns]
        total_width = sum(col_widths)
        scaling_factor = 1.0
        if total_width > max_table_width - 20:
            scaling_factor = (max_table_width - 20) / total_width
            col_widths = [w * scaling_factor for w in col_widths]

        current_x = x_start + 10
        c.setFont("DejaVuSans-Bold", font_size)
        row_height = font_size + 6  # Высота строки таблицы
        c.setLineWidth(0.5)
        height = c._pagesize[1]

        # Заголовок таблицы
        for i, col in enumerate(table_columns):
            c.drawString(current_x + 2, y_position, col)
            c.rect(current_x, y_position - 2, col_widths[i], row_height)
            current_x += col_widths[i]
        y_position -= row_height

        c.setFont("DejaVuSans", font_size)
        for index, row in table_data.iterrows():
            if y_position < 100:
                c.showPage()
                y_position = height - 50
                # Рисуем заголовок таблицы на новой странице
                current_x = x_start + 10
                c.setFont("DejaVuSans-Bold", font_size)
                for i, col in enumerate(table_columns):
                    c.drawString(current_x + 2, y_position, col)
                    c.rect(current_x, y_position - 2, col_widths[i], row_height)
                    current_x += col_widths[i]
                y_position -= row_height
                c.setFont("DejaVuSans", font_size)

            current_x = x_start + 10
            for i, col in enumerate(table_columns):
                c.drawString(current_x + 2, y_position, str(row[col]))
                c.rect(current_x, y_position - 2, col_widths[i], row_height)
                current_x += col_widths[i]
            y_position -= row_height

        return y_position

    def add_photos_to_pdf(self, c, photo_data, project, photo_zone_width, y_top, y_bottom, x_start):
        if 'Название проекта' in photo_data.columns:
            photo_rows = photo_data.loc[photo_data['Название проекта'] == project]
            if not photo_rows.empty:
                for _, photo_row in photo_rows.iterrows():
                    if 'Путь' in photo_row:
                        photo_path = photo_row['Путь']
                        if os.path.exists(photo_path):
                            try:
                                img = ImageReader(photo_path)
                                img_width, img_height = img.getSize()

                                # Доступная высота для изображения
                                available_height = y_top - y_bottom - 10  # 10 - небольшой отступ
                                # Доступная ширина
                                available_width = photo_zone_width - 20

                                # Проверяем, что доступная высота положительная
                                if available_height <= 0 or available_width <= 0:
                                    self.log_signal.emit(
                                        f"Предупреждение: недостаточно места для размещения изображения для проекта '{project}'."
                                    )
                                    continue

                                # Масштабируем изображение пропорционально, чтобы оно поместилось в доступное пространство
                                scale_factor = min(available_width / img_width, available_height / img_height)
                                display_width = img_width * scale_factor
                                display_height = img_height * scale_factor

                                # Позиция изображения по вертикали
                                y_position = y_top - display_height

                                c.drawImage(
                                    img,
                                    x_start + 10,
                                    y_position,
                                    width=display_width,
                                    height=display_height,
                                    preserveAspectRatio=True,
                                    mask='auto'
                                )
                                # Изображение не должно перекрывать таблицу и текст
                                break
                            except Exception as img_e:
                                self.log_signal.emit(f"Ошибка при добавлении фотографии {photo_path}: {img_e}")
                        else:
                            self.log_signal.emit(f"Фотография для проекта {project} не найдена: {photo_path}")
                    else:
                        self.log_signal.emit(f"Предупреждение: В данных фотографий отсутствует столбец 'Путь'.")
            else:
                self.log_signal.emit(f"Фотографии для проекта '{project}' не найдены.")
        else:
            self.log_signal.emit("Предупреждение: В данных фотографий отсутствует столбец 'Название проекта'.")

    def calculate_column_width(self, series, col_name, font_name='DejaVuSans', font_size=8):
        max_width_data = series.astype(str).apply(lambda x: self.string_width(x, font_name, font_size)).max()
        col_header_width = self.string_width(col_name, "DejaVuSans-Bold", font_size)
        return max(max_width_data, col_header_width) + 10

    def string_width(self, text, font_name, font_size):
        return pdfmetrics.stringWidth(text, font_name, font_size)

    def draw_text(self, canvas_obj, text, x, y, font_name, font_size):
        canvas_obj.setFont(font_name, font_size)
        canvas_obj.drawString(x, y, text)

    # Новый метод для рисования текста с переносом строк
    def draw_wrapped_text(self, canvas_obj, text, x, y, font_name, font_size, max_width):
        canvas_obj.setFont(font_name, font_size)
        words = text.split(' ')
        lines = []
        current_line = ''
        for word in words:
            test_line = current_line + ' ' + word if current_line else word
            if self.string_width(test_line, font_name, font_size) <= max_width:
                current_line = test_line
            else:
                lines.append(current_line)
                current_line = word
        if current_line:
            lines.append(current_line)
        for line in lines:
            canvas_obj.drawString(x, y, line)
            y -= font_size + 4  # Задаем интервал между строками
        return y


class PDFGeneratorApp(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Конвертер Excel в PDF")
        self.folder_paths = []
        self.save_path = ""
        self.photo_files = []

        main_widget = QWidget()
        self.setCentralWidget(main_widget)

        main_layout = QVBoxLayout()
        main_widget.setLayout(main_layout)

        self.folder_listbox = QListWidget()
        main_layout.addWidget(self.folder_listbox)

        folder_buttons_layout = QHBoxLayout()
        main_layout.addLayout(folder_buttons_layout)

        self.add_folder_button = QPushButton("Добавить папку")
        self.add_folder_button.clicked.connect(self.add_folder)
        folder_buttons_layout.addWidget(self.add_folder_button)

        self.remove_folder_button = QPushButton("Удалить папку")
        self.remove_folder_button.clicked.connect(self.remove_folder)
        folder_buttons_layout.addWidget(self.remove_folder_button)

        self.save_path_button = QPushButton("Выбрать путь для сохранения PDF")
        self.save_path_button.clicked.connect(self.choose_save_path)
        main_layout.addWidget(self.save_path_button)

        self.save_path_label = QLabel("Путь для сохранения PDF не выбран")
        main_layout.addWidget(self.save_path_label)

        self.photo_files_button = QPushButton("Выбрать файлы с фотографиями")
        self.photo_files_button.clicked.connect(self.choose_photo_files)
        main_layout.addWidget(self.photo_files_button)

        self.photo_files_label = QLabel("Файлы с фотографиями не выбраны")
        main_layout.addWidget(self.photo_files_label)

        self.start_button = QPushButton("Запуск")
        self.start_button.clicked.connect(self.start_processing)
        main_layout.addWidget(self.start_button)

        self.close_button = QPushButton("Закрыть")
        self.close_button.clicked.connect(self.close)
        main_layout.addWidget(self.close_button)

        self.log_text_edit = QTextEdit()
        self.log_text_edit.setReadOnly(True)
        main_layout.addWidget(self.log_text_edit)

        self.apply_styles()

    def apply_styles(self):
        stylesheet = """
        QMainWindow {
            background-color: #f0f0f0;
        }
        QPushButton {
            background-color: #4CAF50;
            color: white;
            padding: 10px 20px;
            border: none;
            border-radius: 5px;
            font-size: 14px;
            margin: 5px;
        }
        QPushButton:hover {
            background-color: #45a049;
        }
        QListWidget {
            background-color: white;
            border: 1px solid #ddd;
            border-radius: 5px;
            padding: 10px;
            font-size: 14px;
        }
        QLabel {
            font-size: 14px;
            color: #333;
        }
        QTextEdit {
            background-color: white;
            border: 1px solid #ddd;
            border-radius: 5px;
            padding: 10px;
            font-size: 12px;
        }
        """
        self.setStyleSheet(stylesheet)

    def add_folder(self):
        folder_path = QFileDialog.getExistingDirectory(self, "Выберите папку")
        if folder_path:
            if folder_path not in self.folder_paths:
                self.folder_paths.append(folder_path)
                self.folder_listbox.addItem(folder_path)
                self.log_info(f"Добавлена папка: {folder_path}")
            else:
                QMessageBox.information(self, "Информация", "Эта папка уже добавлена.")
                self.log_info(f"Попытка добавить уже существующую папку: {folder_path}")

    def remove_folder(self):
        selected_items = self.folder_listbox.selectedItems()
        if not selected_items:
            QMessageBox.information(self, "Информация", "Пожалуйста, выберите папку для удаления.")
            return
        for item in selected_items:
            self.folder_paths.remove(item.text())
            self.folder_listbox.takeItem(self.folder_listbox.row(item))
            self.log_info(f"Удалена папка: {item.text()}")

    def choose_save_path(self):
        save_path = QFileDialog.getExistingDirectory(self, "Выберите путь для сохранения PDF")
        if save_path:
            self.save_path = save_path
            self.save_path_label.setText(f"Сохранение в: {self.save_path}")
            self.log_info(f"Выбран путь для сохранения PDF: {self.save_path}")

    def choose_photo_files(self):
        file_paths, _ = QFileDialog.getOpenFileNames(
            self, "Выберите файлы с фотографиями", "", "Excel files (*.xlsx *.xls)"
        )
        if file_paths:
            self.photo_files = file_paths
            display_paths = ', '.join([os.path.basename(path) for path in file_paths])
            self.photo_files_label.setText(f"Выбраны файлы: {display_paths}")
            self.log_info(f"Выбраны файлы с фотографиями: {display_paths}")

    def start_processing(self):
        if not self.folder_paths:
            QMessageBox.warning(self, "Ошибка", "Пожалуйста, выберите хотя бы одну папку с файлами Excel.")
            self.log_error("Ошибка: не выбрана ни одна папка с файлами Excel.")
            return
        if not self.save_path:
            QMessageBox.warning(self, "Ошибка", "Пожалуйста, выберите путь для сохранения PDF файлов.")
            self.log_error("Ошибка: не выбран путь для сохранения PDF файлов.")
            return
        if not self.photo_files:
            QMessageBox.warning(self, "Ошибка", "Пожалуйста, выберите файлы с фотографиями.")
            self.log_error("Ошибка: не выбраны файлы с фотографиями.")
            return

        self.start_button.setEnabled(False)
        self.pdf_thread = PDFGeneratorThread(self.folder_paths, self.save_path, self.photo_files)
        self.pdf_thread.log_signal.connect(self.log_info)
        self.pdf_thread.finished.connect(self.processing_finished)
        self.pdf_thread.start()
        self.log_info("Начато выполнение процесса генерации PDF.")

    def processing_finished(self):
        self.start_button.setEnabled(True)
        self.log_info("Процесс генерации PDF завершен.")

    def log_info(self, message):
        self.log_text_edit.append(f"INFO: {message}")
        logging.info(message)

    def log_error(self, message):
        self.log_text_edit.append(f"ERROR: {message}")
        logging.error(message)


if __name__ == "__main__":
    app = QApplication([])
    window = PDFGeneratorApp()
    window.show()
    app.exec()
Необходимо скорректировать код. В логах по некоторым проектам выводится информация, что не удалось добавить изображение, так как не хватил места. Нужно поправить логику размещения изображения для таких проектов. Нужно чтобы такие проекты переносились на следующий лист и изображение добавлялось по стандартной логике. Эту логику необходимо применять только для тех проектов, где изображение не помещается стандартным способом, для остальных все должно остаться так же. Перепиши весь этот код с учетом модификации