from __future__ import annotations

from pathlib import Path
import shutil
import re

from PySide6.QtCore import Qt, QUrl, QEvent, QByteArray
from PySide6.QtGui import QDesktopServices, QColor, QGuiApplication, QAction, QTextCursor, QTextTableFormat, QTextCharFormat, QFont, QTextTable, QIcon, QPixmap, QPainter, QPen, QTextLength, QTextFrameFormat, QTextDocumentFragment
from PySide6.QtWidgets import (
    QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QTableWidget,
    QTableWidgetItem, QAbstractItemView, QHeaderView, QMessageBox, QFileDialog,
    QDialog, QListWidget, QListWidgetItem, QComboBox, QLineEdit, QCheckBox, QTextEdit, QDialogButtonBox, QMenu, QInputDialog,
    QToolBar, QFontComboBox, QSizePolicy, QToolButton, QSpinBox, QFormLayout
)

from mi100.repository import Repo
from mi100.services.pdf_documents import build_templated_client_document_pdf
from mi100.timeutil import today_local_date
from mi100.ui.delegates import RowTintDelegate
from mi100.ui.email_send_dialog import EmailSendDialog


BTN_GREEN = (
    "QPushButton{background:#4FAF7A;color:#FFFFFF;border:1px solid #459E6E;"
    "border-radius:4px;padding:4px 10px;font-weight:600;}"
    "QPushButton:hover{background:#459E6E;}"
)
BTN_ORANGE = (
    "QPushButton{background:#FFD8A8;color:#8E6129;border:1px solid #E5BF90;"
    "border-radius:4px;padding:4px 10px;font-weight:600;}"
    "QPushButton:hover{background:#E5BF90;}"
)
BTN_RED = (
    "QPushButton{background:#FFA8A8;color:#7C2D12;border:1px solid #E59494;"
    "border-radius:4px;padding:4px 10px;font-weight:600;}"
    "QPushButton:hover{background:#E59494;}"
)
BTN_GRAY=("QPushButton{background:#EBEBEB;color:#374151;border:1px solid #D7D7D7;"
"border-radius:4px;padding:4px 10px;font-weight:600;}"
"QPushButton:hover{background:#D7D7D7;}")

BTN_EMAIL = (
    "QPushButton{background:#A0CFFF;color:#1C4064;border:1px solid #8CB7E6;"
    "border-radius:4px;padding:4px 10px;font-weight:600;}"
    "QPushButton:hover{background:#8CB7E6;}"
)


SHORTCODES = [
    ('{doc_number}', 'Номер документа для шаблонів листів'),
    ('{recipient_name}', 'Ім’я або назва одержувача для шаблонів листів'),
    ('[[DOC_NUMBER]]', 'Номер рахунку або акту'),
    ('[[CREATED_DATE]]', 'Дата створення документа'),
    ('[[PROJECTS_LIST]]', 'Список обраних проєктів'),
    ('[[PROJECTS_TABLE]]', 'Усі валютні таблиці документа'),
    ('[[PROJECTS_TABLE_ALL]]', 'Усі валютні таблиці документа'),
    ('[[CURRENCY_SECTIONS]]', 'Усі валютні секції з таблицями та підсумками'),
    ('[[CURRENCIES_LIST]]', 'Список валют документа'),
    ('[[PROJECT_PRICE]]', 'Вартість першого або єдиного обраного проєкту'),
    ('[[PAYER_REQUISITES]]', 'Реквізити замовника з вкладки «Дані замовника»'),
    ('[[SUPPLIER_REQUISITES]]', 'Реквізити студії'),
    ('[[TOTAL_AMOUNT]]', 'Підсумки по всіх валютах із прописом словами'),
    ('[[TOTAL_AMOUNT_NUMBER]]', 'Підсумки по всіх валютах лише числом'),
    ('[[TOTALS_BY_CURRENCY]]', 'Підсумки по всіх валютах лише числом'),
    ('[[TOTALS_WORDS_BY_CURRENCY]]', 'Підсумки по всіх валютах із прописом словами'),
    ('[[PROJECTS_TABLE_UAH]]', 'Лише гривнева таблиця'),
    ('[[PROJECTS_TABLE_USD]]', 'Лише доларова таблиця'),
    ('[[PROJECTS_TABLE_EUR]]', 'Лише таблиця в євро'),
    ('[[PROJECTS_TABLE_PLN]]', 'Лише таблиця в злотих'),
    ('[[PROJECTS_TABLE_CZK]]', 'Лише таблиця в чеських кронах'),
    ('[[PROJECTS_TABLE_RON]]', 'Лише таблиця в леях'),
    ('[[PROJECTS_TABLE_GBP]]', 'Лише таблиця у фунтах'),
    ('[[PROJECTS_TABLE_CHF]]', 'Лише таблиця у франках'),
    ('[[CURRENCY_SECTION_UAH]]', 'Повна гривнева секція'),
    ('[[CURRENCY_SECTION_USD]]', 'Повна доларова секція'),
    ('[[CURRENCY_SECTION_EUR]]', 'Повна секція в євро'),
    ('[[CURRENCY_SECTION_PLN]]', 'Повна секція у злотих'),
    ('[[CURRENCY_SECTION_CZK]]', 'Повна секція у чеських кронах'),
    ('[[CURRENCY_SECTION_RON]]', 'Повна секція у леях'),
    ('[[CURRENCY_SECTION_GBP]]', 'Повна секція у фунтах'),
    ('[[CURRENCY_SECTION_CHF]]', 'Повна секція у франках'),
    ('[[TOTAL_AMOUNT_UAH]]', 'Підсумок у гривнях числом'),
    ('[[TOTAL_AMOUNT_USD]]', 'Підсумок у доларах числом'),
    ('[[TOTAL_AMOUNT_EUR]]', 'Підсумок у євро числом'),
    ('[[TOTAL_AMOUNT_PLN]]', 'Підсумок у злотих числом'),
    ('[[TOTAL_AMOUNT_CZK]]', 'Підсумок у чеських кронах числом'),
    ('[[TOTAL_AMOUNT_RON]]', 'Підсумок у леях числом'),
    ('[[TOTAL_AMOUNT_GBP]]', 'Підсумок у фунтах числом'),
    ('[[TOTAL_AMOUNT_CHF]]', 'Підсумок у франках числом'),
    ('[[TOTAL_AMOUNT_WORDS_UAH]]', 'Підсумок у гривнях прописом'),
    ('[[TOTAL_AMOUNT_WORDS_USD]]', 'Підсумок у доларах прописом'),
    ('[[TOTAL_AMOUNT_WORDS_EUR]]', 'Підсумок у євро прописом'),
    ('[[TOTAL_AMOUNT_WORDS_PLN]]', 'Підсумок у злотих прописом'),
    ('[[TOTAL_AMOUNT_WORDS_CZK]]', 'Підсумок у чеських кронах прописом'),
    ('[[TOTAL_AMOUNT_WORDS_RON]]', 'Підсумок у леях прописом'),
    ('[[TOTAL_AMOUNT_WORDS_GBP]]', 'Підсумок у фунтах прописом'),
    ('[[TOTAL_AMOUNT_WORDS_CHF]]', 'Підсумок у франках прописом'),
]

def _format_currency_totals(repo: Repo, rows: list[dict], amount_key: str, currency_keys: tuple[str, ...]) -> str:
    totals: dict[str, float] = {}
    for row in rows:
        cur = ''
        for key in currency_keys:
            val = str((row or {}).get(key) or '').strip()
            if val:
                cur = val
                break
        cur = cur or 'UAH'
        totals[cur] = totals.get(cur, 0.0) + float((row or {}).get(amount_key) or 0.0)
    return repo.format_currency_map(totals, include_zero=True) if totals else repo.format_currency_map({'UAH': 0.0}, include_zero=True)

def _merge_table_css(style: str) -> str:
    style = (style or '').strip().rstrip(';')
    bits = [s.strip() for s in style.split(';') if s.strip()]
    keep = [b for b in bits if not b.lower().startswith('border') and not b.lower().startswith('border-collapse')]
    keep.extend(['border-collapse:collapse', 'border:1.4px solid #000000'])
    return ';'.join(keep) + ';'


def _merge_cell_css(style: str) -> str:
    style = (style or '').strip().rstrip(';')
    bits = [s.strip() for s in style.split(';') if s.strip()]
    keep = [b for b in bits if not b.lower().startswith('border') and not b.lower().startswith('padding')]
    keep.extend(['border:1.4px solid #000000', 'padding:4px'])
    return ';'.join(keep) + ';'


def _replace_style_attr(attrs: str, transform) -> str:
    match = re.search(r"style\s*=\s*([\"'])(.*?)\1", attrs, flags=re.I | re.S)
    if not match:
        return ''
    new_style = transform(match.group(2))
    return attrs[:match.start()] + f'style={match.group(1)}{new_style}{match.group(1)}' + attrs[match.end():]




def _decode_mime_bytes(data) -> str:
    if isinstance(data, QByteArray):
        raw = bytes(data)
    else:
        raw = bytes(data or b'')
    if not raw:
        return ''
    for enc in ('utf-8', 'utf-16', 'cp1251', 'cp1252', 'latin1'):
        try:
            return raw.decode(enc)
        except Exception:
            continue
    return raw.decode('utf-8', errors='ignore')


def _extract_html_fragment(raw_html: str) -> str:
    html = str(raw_html or '')
    if not html:
        return ''
    start = re.search(r'StartFragment:(\d+)', html, flags=re.I)
    end = re.search(r'EndFragment:(\d+)', html, flags=re.I)
    if start and end:
        try:
            s = int(start.group(1))
            e = int(end.group(1))
            fragment = html[s:e]
            if fragment.strip():
                return fragment
        except Exception:
            pass
    marker = re.search(r'<!--StartFragment-->(.*)<!--EndFragment-->', html, flags=re.I | re.S)
    if marker:
        return marker.group(1)
    return html


def _extract_html_from_mime(source) -> str:
    if source is None:
        return ''
    candidates = []
    try:
        if source.hasHtml():
            candidates.append(source.html())
    except Exception:
        pass
    for fmt in (
        'text/html',
        'application/x-qt-windows-mime;value="HTML Format"',
        'application/x-qt-windows-mime;value="Rich HTML Format"',
    ):
        try:
            if source.hasFormat(fmt):
                candidates.append(_decode_mime_bytes(source.data(fmt)))
        except Exception:
            pass
    for candidate in candidates:
        fragment = _extract_html_fragment(candidate)
        if '<table' in fragment.lower() or '<p' in fragment.lower() or '<div' in fragment.lower() or '<span' in fragment.lower():
            return fragment
    return candidates[0] if candidates else ''

def _normalize_pasted_html(html: str) -> str:
    html = str(html or '')
    if not html:
        return html
    html = re.sub(r'<!--.*?-->', '', html, flags=re.S)
    html = re.sub(r'</?o:p[^>]*>', '', html, flags=re.I)
    html = html.replace('mso-line-height-rule:exactly;', '')
    html = html.replace('mso-line-height-rule:exactly', '')

    def _table_style(match):
        attrs = match.group(1) or ''
        replaced = _replace_style_attr(attrs, _merge_table_css)
        if replaced:
            attrs = replaced
        else:
            attrs += ' style="border-collapse:collapse;border:1.4px solid #000000;"'
        return f'<table{attrs}>'

    def _cell_style(match):
        tag = match.group(1)
        attrs = match.group(2) or ''
        replaced = _replace_style_attr(attrs, _merge_cell_css)
        if replaced:
            attrs = replaced
        else:
            attrs += ' style="border:1.4px solid #000000;padding:4px;"'
        return f'<{tag}{attrs}>'

    html = re.sub(r'<table([^>]*)>', _table_style, html, flags=re.I)
    html = re.sub(r'<(td|th)([^>]*)>', _cell_style, html, flags=re.I)
    return html


class ShortcodeTextEdit(QTextEdit):
    def contextMenuEvent(self, event):
        menu = self.createStandardContextMenu()
        table = self.current_table()
        if table is not None:
            insert_menu = menu.addMenu('Вставити')
            for text, slot in [('Вставити стовпці зліва', self._insert_column_left), ('Вставити стовпці справа', self._insert_column_right), ('Вставити рядки вище', self._insert_row_above), ('Вставити рядки нижче', self._insert_row_below)]:
                act = QAction(text, insert_menu); act.triggered.connect(slot); insert_menu.addAction(act)
            align_menu = menu.addMenu('Прив’язка тексту у таблиці')
            for text, mode in [('зліва','left'),('центр','center'),('справа','right')]:
                act = QAction(text, align_menu); act.triggered.connect(lambda _=False, m=mode: self._set_cell_alignment(m)); align_menu.addAction(act)
            menu.addSeparator()
        submenu = menu.addMenu('Шорткоди')
        for code, desc in SHORTCODES:
            act = QAction(f'{code} — {desc}', submenu)
            act.triggered.connect(lambda _=False, value=code: self._copy_shortcode(value))
            submenu.addAction(act)
        menu.exec(event.globalPos())

    def _current_cell(self):
        table = self.current_table()
        cursor = self.textCursor()
        if table is None: return None, -1, -1, cursor
        cell = table.cellAt(cursor)
        return table, cell.row() if cell.isValid() else -1, cell.column() if cell.isValid() else -1, cursor

    def _insert_row_above(self):
        table,row,col,cursor = self._current_cell()
        if table is not None and row >= 0: table.insertRows(row, 1)

    def _insert_row_below(self):
        table,row,col,cursor = self._current_cell()
        if table is not None and row >= 0: table.insertRows(row+1, 1)

    def _insert_column_left(self):
        table,row,col,cursor = self._current_cell()
        if table is not None and col >= 0: table.insertColumns(col, 1)

    def _insert_column_right(self):
        table,row,col,cursor = self._current_cell()
        if table is not None and col >= 0: table.insertColumns(col+1, 1)

    def _set_cell_alignment(self, mode: str):
        table,row,col,cursor = self._current_cell()
        if table is None: return
        block_fmt = cursor.blockFormat()
        if mode == 'center':
            block_fmt.setAlignment(Qt.AlignHCenter)
        elif mode == 'right':
            block_fmt.setAlignment(Qt.AlignRight)
        else:
            block_fmt.setAlignment(Qt.AlignLeft)
        cursor.setBlockFormat(block_fmt)

    def canInsertFromMimeData(self, source):
        return bool(source and (source.hasHtml() or source.hasText() or source.hasUrls())) or super().canInsertFromMimeData(source)

    def insertFromMimeData(self, source):
        html = _extract_html_from_mime(source)
        if html:
            html = _normalize_pasted_html(html)
            cursor = self.textCursor()
            cursor.beginEditBlock()
            cursor.insertHtml(html)
            cursor.endEditBlock()
            return
        super().insertFromMimeData(source)

    def _copy_shortcode(self, value: str) -> None:
        QGuiApplication.clipboard().setText(value)

    def current_table(self):
        return self.textCursor().currentTable()


    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._table_resize_active = False
        self._table_resize_table = None
        self._table_resize_start_x = 0
        self._table_resize_start_width = 100.0
        self.viewport().setMouseTracking(True)

    def _table_rect(self, table):
        try:
            start = self.cursorRect(table.firstCursorPosition())
            end = self.cursorRect(table.lastCursorPosition())
            left = min(start.left(), end.left())
            top = min(start.top(), end.top())
            right = max(start.right(), end.right())
            bottom = max(start.bottom(), end.bottom())
            return left, top, right, bottom
        except Exception:
            return None

    def _table_at_handle(self, pos):
        cursor = self.cursorForPosition(pos)
        table = cursor.currentTable()
        if table is None:
            return None
        rect = self._table_rect(table)
        if not rect:
            return None
        left, top, right, bottom = rect
        if right - 16 <= pos.x() <= right + 8 and bottom - 16 <= pos.y() <= bottom + 8:
            return table
        return None

    def mousePressEvent(self, event):
        if event.button() == Qt.LeftButton:
            table = self._table_at_handle(event.position().toPoint() if hasattr(event, 'position') else event.pos())
            if table is not None:
                self._table_resize_active = True
                self._table_resize_table = table
                p = event.position().toPoint() if hasattr(event, 'position') else event.pos()
                self._table_resize_start_x = p.x()
                fmt = table.format()
                width = fmt.width()
                self._table_resize_start_width = float(width.rawValue()) if width.type() == QTextLength.PercentageLength else 100.0
                self.viewport().setCursor(Qt.SizeFDiagCursor)
                event.accept()
                return
        super().mousePressEvent(event)

    def mouseMoveEvent(self, event):
        pos = event.position().toPoint() if hasattr(event, 'position') else event.pos()
        if self._table_resize_active and self._table_resize_table is not None:
            rect = self._table_rect(self._table_resize_table)
            if rect:
                left, top, right, bottom = rect
                viewport_width = max(100, self.viewport().width() - 24)
                delta = pos.x() - self._table_resize_start_x
                percent_delta = (delta / viewport_width) * 100.0
                new_width = max(20.0, min(100.0, self._table_resize_start_width + percent_delta))
                fmt = self._table_resize_table.format()
                fmt.setWidth(QTextLength(QTextLength.PercentageLength, new_width))
                self._table_resize_table.setFormat(fmt)
            event.accept()
            return
        table = self._table_at_handle(pos)
        self.viewport().setCursor(Qt.SizeFDiagCursor if table is not None else Qt.IBeamCursor)
        super().mouseMoveEvent(event)

    def mouseReleaseEvent(self, event):
        if self._table_resize_active:
            self._table_resize_active = False
            self._table_resize_table = None
            self.viewport().setCursor(Qt.IBeamCursor)
            event.accept()
            return
        super().mouseReleaseEvent(event)


def _make_align_icon(mode: str) -> QIcon:
    pix = QPixmap(26, 18)
    pix.fill(Qt.transparent)
    painter = QPainter(pix)
    painter.setRenderHint(QPainter.Antialiasing, False)
    pen = QPen(QColor('#4B5563'))
    pen.setWidth(2)
    painter.setPen(pen)
    y_positions = [3, 7, 11, 15]
    widths = [16, 20, 14, 18]
    for y, w in zip(y_positions, widths):
        if mode == 'left':
            x1 = 3
        elif mode == 'center':
            x1 = (26 - w) // 2
        elif mode == 'right':
            x1 = 26 - w - 3
        else:
            x1 = 3
            w = 20
        painter.drawLine(x1, y, x1 + w, y)
    painter.end()
    return QIcon(pix)


class TableInsertDialog(QDialog):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setWindowTitle('Таблиця')
        self.resize(360, 220)
        layout = QVBoxLayout(self)
        form = QFormLayout()

        self.rows_box = QSpinBox()
        self.rows_box.setRange(1, 50)
        self.rows_box.setValue(2)
        form.addRow('Рядків:', self.rows_box)

        self.cols_box = QSpinBox()
        self.cols_box.setRange(1, 20)
        self.cols_box.setValue(2)
        form.addRow('Колонок:', self.cols_box)

        self.width_box = QSpinBox()
        self.width_box.setRange(20, 100)
        self.width_box.setSuffix(' %')
        self.width_box.setValue(100)
        form.addRow('Ширина:', self.width_box)

        self.align_box = QComboBox()
        self.align_box.addItem('По лівому краю', 'left')
        self.align_box.addItem('По центру', 'center')
        self.align_box.addItem('По правому краю', 'right')
        form.addRow('Вирівнювання:', self.align_box)

        self.borders_box = QCheckBox('Видимі межі таблиці')
        self.borders_box.setChecked(True)
        form.addRow('', self.borders_box)

        layout.addLayout(form)
        buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
        buttons.accepted.connect(self.accept)
        buttons.rejected.connect(self.reject)
        layout.addWidget(buttons)

    def values(self) -> dict:
        return {
            'rows': int(self.rows_box.value()),
            'cols': int(self.cols_box.value()),
            'width_percent': int(self.width_box.value()),
            'alignment': str(self.align_box.currentData() or 'left'),
            'borders_visible': bool(self.borders_box.isChecked()),
        }


class TemplateEditorDialog(QDialog):
    def __init__(self, repo: Repo, client_id: int, doc_type: str, parent=None):
        super().__init__(parent)
        self.repo = repo
        self.client_id = int(client_id)
        self.doc_type = str(doc_type or '').upper().strip()
        self.setWindowTitle('Шаблон рахунку' if self.doc_type == 'INVOICE' else 'Шаблон акту')
        self.resize(1040, 760)

        layout = QVBoxLayout(self)
        hint = QLabel('Редагуйте шаблон як у текстовому редакторі. Правою кнопкою миші відкрийте список шорткодів. Натискання по шорткоду копіює його в буфер.')
        hint.setWordWrap(True)
        layout.addWidget(hint)

        self.editor = ShortcodeTextEdit()
        self.editor.setAcceptRichText(True)
        self.editor.setStyleSheet('QTextEdit{background:#FFFFFF;}')

        self._build_toolbar(layout)

        saved = self.repo.get_client_document_template(self.client_id, self.doc_type)
        if '<html' in (saved or '').lower() or '<body' in (saved or '').lower():
            self.editor.setHtml(saved)
        else:
            self.editor.setPlainText(saved)
        self.editor.cursorPositionChanged.connect(self._sync_toolbar_state)
        layout.addWidget(self.editor, 1)
        buttons = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Cancel)
        buttons.accepted.connect(self._save)
        buttons.rejected.connect(self.reject)
        layout.addWidget(buttons)
        self._sync_toolbar_state()

    def _build_toolbar(self, layout: QVBoxLayout) -> None:
        self.toolbar = QToolBar()
        self.toolbar.setMovable(False)
        self.toolbar.setStyleSheet(
            'QToolBar{spacing:3px;border:1px solid #D5D9DE;border-radius:6px;background:#F8FAFC;padding:4px;}'
            'QToolButton{padding:4px 6px;border:1px solid transparent;border-radius:4px;min-width:28px;min-height:24px;}'
            'QToolButton:hover{background:#EBEBEB;border-color:#D7D7D7;}'
            'QToolButton:checked{background:#A0CFFF;border-color:#8EBCEB;}'
            'QComboBox,QFontComboBox{min-height:26px;padding:1px 6px;}'
        )

        self.font_box = QFontComboBox()
        self.font_box.setMinimumWidth(170)
        self.font_box.currentFontChanged.connect(self._set_font_family)
        self.toolbar.addWidget(self.font_box)

        self.size_box = QComboBox()
        self.size_box.setEditable(True)
        self.size_box.setMinimumWidth(72)
        for size in ['8', '9', '10', '11', '12', '14', '16', '18', '20', '24', '28', '36']:
            self.size_box.addItem(size)
        self.size_box.lineEdit().editingFinished.connect(self._apply_font_size_from_box)
        self.size_box.currentTextChanged.connect(self._apply_font_size_text_changed)
        self.toolbar.addWidget(self.size_box)

        self.toolbar.addSeparator()

        def add_btn(text: str, tooltip: str, slot, *, checkable: bool = False, icon: QIcon | None = None):
            btn = QToolButton()
            btn.setText(text)
            if icon is not None:
                btn.setIcon(icon)
            btn.setToolTip(tooltip)
            btn.setCheckable(checkable)
            btn.clicked.connect(slot)
            self.toolbar.addWidget(btn)
            return btn

        self.btn_bold = add_btn('Ж', 'Жирний', self._toggle_bold, checkable=True)
        self.btn_italic = add_btn('К', 'Курсив', self._toggle_italic, checkable=True)
        self.btn_underline = add_btn('Ч', 'Підкреслення', self._toggle_underline, checkable=True)
        self.btn_upper = add_btn('Aa', 'Верхній / нижній регістр для виділеного тексту', self._toggle_case_selection)

        self.toolbar.addSeparator()
        self.btn_left = add_btn('', 'По лівому краю', lambda: self._set_alignment('left'), icon=_make_align_icon('left'))
        self.btn_center = add_btn('', 'По центру', lambda: self._set_alignment('center'), icon=_make_align_icon('center'))
        self.btn_right = add_btn('', 'По правому краю', lambda: self._set_alignment('right'), icon=_make_align_icon('right'))
        self.btn_justify = add_btn('', 'На всю ширину', lambda: self._set_alignment('justify'), icon=_make_align_icon('justify'))

        self.toolbar.addSeparator()
        self.btn_bullets = add_btn('•', 'Список', self._insert_bulleted_list)
        self.btn_table = add_btn('Таблиця', 'Вставити таблицю', self._insert_table)
        self.btn_table_borders = add_btn('Межі таблиці', 'Показати або приховати межі поточної таблиці', self._toggle_current_table_borders, checkable=True)
        layout.addWidget(self.toolbar)

    def _merge_char_format(self, fmt: QTextCharFormat) -> None:
        cursor = self.editor.textCursor()
        if cursor.hasSelection():
            cursor.mergeCharFormat(fmt)
        self.editor.mergeCurrentCharFormat(fmt)

    def _set_font_family(self, font) -> None:
        fmt = QTextCharFormat()
        fmt.setFontFamily(font.family())
        self._merge_char_format(fmt)

    def _apply_font_size_text_changed(self, value: str) -> None:
        try:
            size = float((value or '').strip().replace(',', '.'))
        except Exception:
            return
        self._set_font_point_size(size)

    def _apply_font_size_from_box(self) -> None:
        value = (self.size_box.currentText() or '').strip()
        try:
            self._set_font_point_size(float(value.replace(',', '.')))
        except Exception:
            pass

    def _set_font_point_size(self, size: float) -> None:
        if size <= 0:
            return
        fmt = QTextCharFormat()
        fmt.setFontPointSize(size)
        self._merge_char_format(fmt)

    def _toggle_bold(self) -> None:
        fmt = QTextCharFormat()
        fmt.setFontWeight(QFont.Bold if self.btn_bold.isChecked() else QFont.Normal)
        self._merge_char_format(fmt)

    def _toggle_italic(self) -> None:
        fmt = QTextCharFormat()
        fmt.setFontItalic(self.btn_italic.isChecked())
        self._merge_char_format(fmt)

    def _toggle_underline(self) -> None:
        fmt = QTextCharFormat()
        fmt.setFontUnderline(self.btn_underline.isChecked())
        self._merge_char_format(fmt)

    def _toggle_case_selection(self) -> None:
        cursor = self.editor.textCursor()
        if not cursor.hasSelection():
            return
        text = cursor.selectedText().replace('\u2029', '\n')
        transformed = text.lower() if text == text.upper() else text.upper()
        cursor.insertText(transformed)

    def _set_alignment(self, mode: str) -> None:
        table = self.editor.current_table()
        if table is not None:
            fmt = table.format()
            if mode == 'center':
                fmt.setAlignment(Qt.AlignHCenter)
            elif mode == 'right':
                fmt.setAlignment(Qt.AlignRight)
            else:
                fmt.setAlignment(Qt.AlignLeft)
            table.setFormat(fmt)
            return
        if mode == 'left':
            self.editor.setAlignment(Qt.AlignLeft)
        elif mode == 'center':
            self.editor.setAlignment(Qt.AlignHCenter)
        elif mode == 'right':
            self.editor.setAlignment(Qt.AlignRight)
        else:
            self.editor.setAlignment(Qt.AlignJustify)

    def _insert_bulleted_list(self) -> None:
        cursor = self.editor.textCursor()
        if cursor.hasSelection():
            text = cursor.selectedText().replace('\u2029', '\n')
            lines = [line.strip() for line in text.splitlines()]
            bulleted = '\n'.join(f'• {line}' if line else '•' for line in lines)
            cursor.insertText(bulleted)
        else:
            cursor.insertText('• ')

    def _insert_table(self) -> None:
        dialog = TableInsertDialog(self)
        if dialog.exec() != QDialog.Accepted:
            return
        data = dialog.values()
        cursor = self.editor.textCursor()
        fmt = QTextTableFormat()
        fmt.setBorder(1.4 if data['borders_visible'] else 0.0)
        fmt.setBorderStyle(QTextFrameFormat.BorderStyle_Solid)
        fmt.setBorderBrush(QColor('#000000'))
        fmt.setCellPadding(4)
        fmt.setCellSpacing(0)
        fmt.setWidth(QTextLength(QTextLength.PercentageLength, float(data['width_percent'])))
        if data['alignment'] == 'center':
            fmt.setAlignment(Qt.AlignHCenter)
        elif data['alignment'] == 'right':
            fmt.setAlignment(Qt.AlignRight)
        else:
            fmt.setAlignment(Qt.AlignLeft)
        cursor.insertTable(int(data['rows']), int(data['cols']), fmt)
        self.btn_table_borders.setChecked(bool(data['borders_visible']))

    def _toggle_current_table_borders(self) -> None:
        table = self.editor.current_table()
        if table is None:
            QMessageBox.information(self, 'Таблиця', 'Поставте курсор всередину таблиці, щоб керувати межами.')
            self.btn_table_borders.setChecked(False)
            return
        fmt = table.format()
        show_borders = self.btn_table_borders.isChecked()
        fmt.setBorder(1.4 if show_borders else 0.0)
        fmt.setBorderStyle(QTextFrameFormat.BorderStyle_Solid)
        fmt.setBorderBrush(QColor('#000000'))
        table.setFormat(fmt)

    def _sync_toolbar_state(self) -> None:
        char_fmt = self.editor.currentCharFormat()
        font = char_fmt.font()
        self.font_box.blockSignals(True)
        self.font_box.setCurrentFont(font)
        self.font_box.blockSignals(False)

        size = char_fmt.fontPointSize() or self.editor.fontPointSize() or font.pointSizeF() or 10.0
        self.size_box.blockSignals(True)
        self.size_box.setCurrentText(str(int(size)) if float(size).is_integer() else f'{size:.1f}')
        self.size_box.blockSignals(False)

        self.btn_bold.setChecked((char_fmt.fontWeight() or font.weight()) >= QFont.Bold)
        self.btn_italic.setChecked(char_fmt.fontItalic() or font.italic())
        self.btn_underline.setChecked(char_fmt.fontUnderline() or font.underline())

        table = self.editor.current_table()
        self.btn_table_borders.setEnabled(table is not None)
        self.btn_table_borders.setChecked(bool(table is not None and float(table.format().border()) > 0.0))

    def _save(self) -> None:
        self.repo.set_client_document_template(self.client_id, self.doc_type, self.editor.toHtml())
        self.accept()


class DocumentCreateDialog(QDialog):
    def __init__(self, repo: Repo, client_id: int, doc_type: str, parent=None, select_all_unpaid: bool = False, select_all_available: bool = False):
        super().__init__(parent)
        self.repo = repo
        self.client_id = int(client_id)
        self.doc_type = doc_type
        self.select_all_unpaid = bool(select_all_unpaid)
        self.select_all_available = bool(select_all_available)
        self.setWindowTitle("Створення рахунку" if doc_type == "INVOICE" else "Створення акту")
        self.resize(820, 620)
        self._project_rows: list[dict] = []
        self._invoice_rows: list[dict] = []

        layout = QVBoxLayout(self)
        layout.setContentsMargins(12, 12, 12, 12)
        layout.setSpacing(10)

        self.info_label = QLabel("")
        self.info_label.setWordWrap(True)
        layout.addWidget(self.info_label)

        self.filter_combo = QComboBox()
        self.filter_combo.setEditable(True)
        self.filter_combo.setInsertPolicy(QComboBox.NoInsert)
        self.filter_combo.setPlaceholderText("Пошук за номером, датою або назвою...")
        self.filter_combo.lineEdit().textChanged.connect(self._apply_filter)
        layout.addWidget(self.filter_combo)

        self.select_all_checkbox = QCheckBox('Рахунок на всі неоплачені проєкти' if self.doc_type == 'INVOICE' else 'Акт на всі неоплачені рахунки')
        self.select_all_checkbox.setChecked(self.select_all_unpaid or self.select_all_available)
        self.select_all_checkbox.toggled.connect(self._toggle_select_all_checkbox)
        layout.addWidget(self.select_all_checkbox)

        self.select_hint = QLabel("")
        self.select_hint.setWordWrap(True)
        layout.addWidget(self.select_hint)

        self.select_list = QListWidget()
        self.select_list.itemChanged.connect(self._update_summary)
        layout.addWidget(self.select_list, 1)

        self.preview_label = QLabel("Проєкти в документі:")
        self.preview_label.setVisible(self.doc_type == "ACT")
        layout.addWidget(self.preview_label)
        self.projects_list = QListWidget()
        self.projects_list.setEnabled(False)
        self.projects_list.setVisible(self.doc_type == "ACT")
        layout.addWidget(self.projects_list, 1 if self.doc_type == "ACT" else 0)

        bottom = QHBoxLayout()
        self.sum_label = QLabel("")
        self.sum_label.setStyleSheet("font-weight:700;")
        bottom.addWidget(self.sum_label)
        bottom.addStretch(1)
        self.generate_btn = QPushButton("Згенерувати PDF")
        self.generate_btn.setStyleSheet(BTN_GREEN)
        self.generate_btn.clicked.connect(self._generate)
        bottom.addWidget(self.generate_btn)
        cancel = QPushButton("Скасувати")
        cancel.clicked.connect(self.reject)
        bottom.addWidget(cancel)
        layout.addLayout(bottom)

        if self.doc_type == "INVOICE":
            self._load_projects()
            if self.select_all_unpaid or self.select_all_available:
                self._preselect_all_unpaid_available()
        else:
            self._load_invoices()
            if self.select_all_available:
                self._preselect_all_invoices_available()
        self._update_summary()

    def _make_item_text_from_project(self, row: dict) -> str:
        suffix = f" • уже в рахунку {row.get('invoice_number')}" if row.get('has_invoice') and row.get('invoice_number') else ""
        return f"№{row['project_number']} • {row['created_date']} • {row['title']} • {self.repo.format_money(float(row['total_amount'] or 0), row.get('currency') or 'UAH')}{suffix}"

    def _make_item_text_from_invoice(self, row: dict) -> str:
        suffix = " • акт уже створено" if row.get('has_act') else ""
        return f"{row['doc_number']} • {row['created_date']} • {self.repo.format_money(float(row.get('total_amount') or 0), row.get('currency') or 'UAH')}{suffix}"

    def _load_projects(self) -> None:
        self._project_rows = self.repo.get_invoice_projects_state(self.client_id)
        self.filter_combo.clear()
        self.filter_combo.addItem("Усі проєкти")
        for row in self._project_rows:
            self.filter_combo.addItem(self._make_item_text_from_project(row))
        self._apply_filter("")

    def _load_invoices(self) -> None:
        self._invoice_rows = self.repo.list_client_invoices_available_for_act(self.client_id)
        self.filter_combo.clear()
        self.filter_combo.addItem("Усі рахунки")
        for row in self._invoice_rows:
            self.filter_combo.addItem(self._make_item_text_from_invoice(row))
        self._apply_filter("")

    def _item_key_text(self, row: dict) -> str:
        if self.doc_type == "INVOICE":
            return f"{row.get('project_number','')} {row.get('created_date','')} {row.get('title','')}"
        return f"{row.get('doc_number','')} {row.get('created_date','')}"

    def _apply_filter(self, text: str) -> None:
        raw_text = (text or "").strip()
        sentinel = 'усі проєкти' if self.doc_type == 'INVOICE' else 'усі рахунки'
        text = '' if raw_text.lower() == sentinel else raw_text.lower()
        self.select_list.blockSignals(True)
        self.select_list.clear()
        if self.doc_type == "INVOICE":
            rows = self._project_rows
            self.info_label.setText("Оберіть один або кілька проєктів для рахунку.")
            self.select_hint.setText("Зелені рядки доступні для рахунку. Сірі вже входять до інших рахунків і недоступні.")
            for row in rows:
                if text and text not in self._item_key_text(row).lower():
                    continue
                item = QListWidgetItem(self._make_item_text_from_project(row))
                item.setData(Qt.UserRole, dict(row))
                if row.get('has_invoice'):
                    item.setFlags(Qt.ItemIsEnabled)
                    item.setBackground(QColor(235, 235, 235))
                    item.setForeground(QColor('#374151'))
                else:
                    item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
                    item.setCheckState(Qt.Checked if (self.select_all_unpaid or self.select_all_available) and int(row.get('client_paid') or 0) == 0 else Qt.Unchecked)
                    item.setBackground(QColor(210, 242, 220))
                    item.setForeground(QColor('#0F5132'))
                self.select_list.addItem(item)
        else:
            rows = self._invoice_rows
            self.info_label.setText("Оберіть один або кілька рахунків. Акт формується на їх основі.")
            self.select_hint.setText("Доступні рахунки — зелені. Сірі — ті, для яких акт уже створено.")
            for row in rows:
                if text and text not in self._item_key_text(row).lower():
                    continue
                item = QListWidgetItem(self._make_item_text_from_invoice(row))
                item.setData(Qt.UserRole, dict(row))
                if row.get('has_act'):
                    item.setFlags(Qt.ItemIsEnabled)
                    item.setBackground(QColor(235, 235, 235))
                    item.setForeground(QColor('#374151'))
                else:
                    item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
                    item.setCheckState(Qt.Unchecked)
                    item.setBackground(QColor(210, 242, 220))
                    item.setForeground(QColor('#0F5132'))
                self.select_list.addItem(item)
        self.select_list.blockSignals(False)
        self._update_summary()

    def _preselect_all_unpaid_available(self) -> None:
        for i in range(self.select_list.count()):
            item = self.select_list.item(i)
            row = item.data(Qt.UserRole) or {}
            if not row.get('has_invoice') and int(row.get('client_paid') or 0) == 0:
                item.setCheckState(Qt.Checked)

    def _preselect_all_invoices_available(self) -> None:
        for i in range(self.select_list.count()):
            item = self.select_list.item(i)
            row = item.data(Qt.UserRole) or {}
            if not row.get('has_act'):
                item.setCheckState(Qt.Checked)

    def _toggle_select_all_checkbox(self, checked: bool) -> None:
        self.select_list.blockSignals(True)
        for i in range(self.select_list.count()):
            item = self.select_list.item(i)
            if not (item.flags() & Qt.ItemIsUserCheckable):
                continue
            row = item.data(Qt.UserRole) or {}
            if self.doc_type == 'INVOICE':
                allowed = not row.get('has_invoice') and int(row.get('client_paid') or 0) == 0
            else:
                allowed = not row.get('has_act')
            item.setCheckState(Qt.Checked if checked and allowed else Qt.Unchecked)
        self.select_list.blockSignals(False)
        self._update_summary()

    def _selected_projects(self) -> list[dict]:
        rows: list[dict] = []
        for i in range(self.select_list.count()):
            item = self.select_list.item(i)
            if item.flags() & Qt.ItemIsUserCheckable and item.checkState() == Qt.Checked:
                rows.append(item.data(Qt.UserRole))
        return rows

    def _selected_invoice_rows(self) -> list[dict]:
        rows: list[dict] = []
        for i in range(self.select_list.count()):
            item = self.select_list.item(i)
            if item.flags() & Qt.ItemIsUserCheckable and item.checkState() == Qt.Checked:
                rows.append(item.data(Qt.UserRole))
        return rows

    def _combined_projects_from_invoices(self, invoice_rows: list[dict]) -> list[dict]:
        seen: set[int] = set()
        projects: list[dict] = []
        for inv in invoice_rows:
            for p in self.repo.get_document_projects(int(inv['id'])):
                pid = int(p.get('project_id') or 0)
                if pid and pid in seen:
                    continue
                if pid:
                    seen.add(pid)
                projects.append(p)
        return projects

    def _update_summary(self) -> None:
        if self.doc_type == "INVOICE":
            projects = self._selected_projects()
            summary = _format_currency_totals(self.repo, projects, 'total_amount', ('currency',))
            self.sum_label.setText('Сума:\n' + summary)
            self.generate_btn.setEnabled(bool(projects))
            return

        invoice_rows = self._selected_invoice_rows()
        projects = self._combined_projects_from_invoices(invoice_rows)
        self.projects_list.clear()
        for row in projects:
            self.projects_list.addItem(
                f"№{row.get('project_number_snapshot') or row.get('project_number')} • "
                f"{row.get('project_date_snapshot') or row.get('created_date')} • "
                f"{row.get('project_title_snapshot') or row.get('title')} • "
                f"{float(row.get('amount_snapshot') if row.get('amount_snapshot') is not None else row.get('total_amount') or 0):.2f} "
                f"{row.get('currency_snapshot') or row.get('currency') or 'UAH'}"
            )
        summary = _format_currency_totals(self.repo, projects, 'amount_snapshot', ('currency_snapshot','currency'))
        self.sum_label.setText('Сума:\n' + summary)
        self.generate_btn.setEnabled(bool(invoice_rows))

    def _generate(self) -> None:
        try:
            client = dict(self.repo.get_client(self.client_id))
            studio = dict(self.repo.get_studio_profile())
            if self.doc_type == "INVOICE":
                projects = self._selected_projects()
                if not projects:
                    QMessageBox.warning(self, "Увага", "Оберіть хоча б один проєкт.")
                    return
                blocked = [p for p in projects if p.get('has_invoice')]
                if blocked:
                    QMessageBox.warning(self, "Увага", "Серед вибраних є проєкти, для яких рахунок уже створено.")
                    return
                doc_number = self.repo.next_document_number("INVOICE")
                created_date = today_local_date()
                template_text = self.repo.get_client_document_template(self.client_id, 'INVOICE')
                generated = build_templated_client_document_pdf(
                    doc_type='INVOICE',
                    doc_number=doc_number,
                    created_date=created_date,
                    client=client,
                    projects=projects,
                    supplier_requisites_text=studio.get("supplier_requisites_text") or "",
                    template_text=template_text,
                )
                self.repo.create_document(
                    doc_type="INVOICE",
                    client_id=self.client_id,
                    voice_actor_id=None,
                    doc_number=doc_number,
                    currency=generated.currency,
                    total_amount=generated.total_amount,
                    created_date=created_date,
                    with_contract=0,
                    contract_number_snapshot=None,
                    contract_date_snapshot=None,
                    file_path=generated.file_path,
                    projects=projects,
                )
                QDesktopServices.openUrl(QUrl.fromLocalFile(generated.file_path))
            else:
                invoice_rows = self._selected_invoice_rows()
                if not invoice_rows:
                    QMessageBox.warning(self, "Увага", "Оберіть хоча б один рахунок.")
                    return
                if any(self.repo.invoice_has_any_act(int(inv['id'])) for inv in invoice_rows):
                    QMessageBox.warning(self, "Увага", "Серед вибраних є рахунки, для яких акт уже створено.")
                    return
                projects = self._combined_projects_from_invoices(invoice_rows)
                doc_number = self.repo.next_document_number("ACT")
                created_date = today_local_date()
                template_text = self.repo.get_client_document_template(self.client_id, 'ACT')
                generated = build_templated_client_document_pdf(
                    doc_type='ACT',
                    doc_number=doc_number,
                    created_date=created_date,
                    client=client,
                    projects=projects,
                    supplier_requisites_text=studio.get("supplier_requisites_text") or "",
                    template_text=template_text,
                )
                invoice_ids = [int(inv['id']) for inv in invoice_rows]
                notes = f"[invoice_ids:{','.join(str(x) for x in invoice_ids)}]"
                all_projects = self.repo.list_projects_for_ids([int(p["project_id"]) for p in projects], client_id=self.client_id)
                self.repo.create_document(
                    doc_type="ACT",
                    client_id=self.client_id,
                    voice_actor_id=None,
                    doc_number=doc_number,
                    related_invoice_id=invoice_ids[0] if len(invoice_ids) == 1 else None,
                    currency=generated.currency,
                    total_amount=generated.total_amount,
                    created_date=created_date,
                    with_contract=0,
                    contract_number_snapshot=None,
                    contract_date_snapshot=None,
                    file_path=generated.file_path,
                    notes=notes,
                    projects=all_projects,
                )
                QDesktopServices.openUrl(QUrl.fromLocalFile(generated.file_path))
            self.accept()
        except Exception as exc:
            QMessageBox.critical(self, "Помилка генерації PDF", f"Не вдалося згенерувати документ.\n\n{exc}")


class ClientDocumentsTab(QWidget):
    def __init__(self, repo: Repo, client_id_provider, parent=None):
        super().__init__(parent)
        self.repo = repo
        self.client_id_provider = client_id_provider
        self._all_rows: list[dict] = []

        layout = QVBoxLayout(self)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(8)

        actions = QHBoxLayout()
        self.add_invoice_btn = QPushButton("Рахунок +")
        self.add_invoice_btn.setStyleSheet(BTN_GREEN)
        self.add_invoice_btn.clicked.connect(self._add_invoice)
        actions.addWidget(self.add_invoice_btn, 0, Qt.AlignLeft)
        self.add_act_btn = QPushButton("Акт +")
        self.add_act_btn.setStyleSheet(BTN_GREEN)
        self.add_act_btn.clicked.connect(self._add_act)
        actions.addWidget(self.add_act_btn, 0, Qt.AlignLeft)
        self.template_invoice_btn = QPushButton("Шаблон рахунку")
        self.template_invoice_btn.setStyleSheet(BTN_GRAY)
        self.template_invoice_btn.clicked.connect(lambda: self._edit_template('INVOICE'))
        self.add_all_unpaid_btn = QPushButton("Рахунок на всі неоплачені проєкти")
        self.add_all_unpaid_btn.setStyleSheet(BTN_GREEN)
        self.add_all_unpaid_btn.clicked.connect(self._add_invoice_all_unpaid)
        actions.addWidget(self.add_all_unpaid_btn, 0, Qt.AlignLeft)
        self.add_all_acts_btn = QPushButton("Акт на всі неоплачені рахунки")
        self.add_all_acts_btn.setStyleSheet(BTN_GREEN)
        self.add_all_acts_btn.clicked.connect(self._add_act_all_unpaid)
        actions.addWidget(self.add_all_acts_btn, 0, Qt.AlignLeft)
        actions.addStretch(1)
        actions.addWidget(self.template_invoice_btn, 0, Qt.AlignRight)
        self.template_act_btn = QPushButton("Шаблон акту")
        self.template_act_btn.setStyleSheet(BTN_GRAY)
        self.template_act_btn.clicked.connect(lambda: self._edit_template('ACT'))
        actions.addWidget(self.template_act_btn, 0, Qt.AlignRight)
        layout.addLayout(actions)

        search_row = QHBoxLayout()
        lbl_search = QLabel("Пошук:"); lbl_search.setStyleSheet("font-weight:700; color:#111827; background:transparent; border:none;"); search_row.addWidget(lbl_search)
        self.search_edit = QLineEdit()
        self.search_edit.setPlaceholderText("Пошук по номеру документа або по даті")
        self.search_edit.textChanged.connect(self._apply_search)
        search_row.addWidget(self.search_edit, 1)
        layout.addLayout(search_row)

        self.table = QTableWidget(0, 6)
        self._row_delegate = RowTintDelegate(self.table)
        self._action_holders: dict[int, tuple[QWidget, str]] = {}
        self.table.setHorizontalHeaderLabels(["Тип", "Номер", "Дата", "Сума", "Файл", "Дії"])
        self.table.verticalHeader().setVisible(False)
        self.table.setEditTriggers(QAbstractItemView.NoEditTriggers)
        self.table.setSelectionBehavior(QAbstractItemView.SelectRows)
        self.table.setSelectionMode(QAbstractItemView.SingleSelection)
        self.table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeToContents)
        self.table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeToContents)
        self.table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeToContents)
        self.table.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeToContents)
        self.table.horizontalHeader().setSectionResizeMode(4, QHeaderView.Stretch)
        self.table.horizontalHeader().setSectionResizeMode(5, QHeaderView.ResizeToContents)
        self.table.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.table.setWordWrap(True)
        self.table.verticalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
        self.table.setItemDelegate(self._row_delegate)
        self.table.setMouseTracking(True)
        self.table.viewport().setMouseTracking(True)
        self.table.cellEntered.connect(self._on_hover_row)
        self.table.viewport().installEventFilter(self)
        layout.addWidget(self.table, 1)

    def refresh(self) -> None:
        client_id = self.client_id_provider()
        self.table.setRowCount(0)
        enabled = bool(client_id)
        self.add_invoice_btn.setEnabled(enabled)
        self.add_act_btn.setEnabled(enabled)
        self.add_all_unpaid_btn.setEnabled(enabled)
        self.add_all_acts_btn.setEnabled(enabled)
        self.template_invoice_btn.setEnabled(enabled)
        self.template_act_btn.setEnabled(enabled)
        if not client_id:
            return
        self._all_rows = self.repo.list_client_documents_grouped(int(client_id), self.search_edit.text())
        self._render_rows(self._all_rows)

    def _render_rows(self, rows: list[dict]) -> None:
        self.table.setRowCount(0)
        self._action_holders = {}
        for row in rows:
            r = self.table.rowCount()
            self.table.insertRow(r)
            bg_key = "green" if str(row.get("doc_type") or "") == "INVOICE" else "yellow"
            marker = str(row.get('_group_marker') or '')
            type_label = str(row.get("doc_type_label") or row.get("doc_type") or "")
            if marker:
                type_label = f"{marker} {type_label}"
            values = [
                type_label,
                str(row.get("doc_number") or ""),
                str(row.get("created_date") or ""),
                f"{float(row.get('total_amount') or 0):.2f} {row.get('currency') or 'UAH'}",
                Path(str(row.get("file_path") or "")).name,
            ]
            for c, value in enumerate(values):
                item = QTableWidgetItem(value)
                item.setData(RowTintDelegate.ROLE_BG_KEY, bg_key)
                if c == 0 and marker:
                    f = item.font()
                    f.setBold(True)
                    f.setPointSize(max(f.pointSize(), 11))
                    item.setFont(f)
                self.table.setItem(r, c, item)

            actions = QWidget()
            actions.setAttribute(Qt.WA_StyledBackground, True)
            actions.setAutoFillBackground(False)
            actions.setStyleSheet('background:transparent; border:none;')
            hlay = QHBoxLayout(actions)
            hlay.setContentsMargins(0, 0, 0, 0)
            hlay.setSpacing(6)
            BTN_W = 88
            BTN_H = 24
            btn_open = QPushButton("Відкрити")
            btn_open.setFixedSize(BTN_W, BTN_H)
            btn_open.setStyleSheet(BTN_ORANGE)
            btn_open.clicked.connect(lambda _=False, p=str(row.get("file_path") or ""): self._open_file(p))
            hlay.addWidget(btn_open, 0, Qt.AlignCenter)
            btn_download = QPushButton("Скачати")
            btn_download.setFixedSize(BTN_W, BTN_H)
            btn_download.setStyleSheet(BTN_ORANGE)
            btn_download.clicked.connect(lambda _=False, p=str(row.get("file_path") or ""): self._download_file(p))
            hlay.addWidget(btn_download, 0, Qt.AlignCenter)
            btn_send = QPushButton("Email")
            btn_send.setFixedSize(74, BTN_H)
            btn_send.setStyleSheet(BTN_EMAIL)
            btn_send.clicked.connect(lambda _=False, d=dict(row): self._send_document_email(d))
            hlay.addWidget(btn_send, 0, Qt.AlignCenter)
            allow_delete = not (str(row.get('doc_type')) == 'INVOICE' and self.repo.invoice_has_any_act(int(row['id'])))
            if allow_delete:
                btn_delete = QPushButton("Видалити")
                btn_delete.setFixedSize(BTN_W, BTN_H)
                btn_delete.setStyleSheet(BTN_RED)
                btn_delete.clicked.connect(lambda _=False, d=dict(row): self._delete_document_row(d))
                hlay.addWidget(btn_delete, 0, Qt.AlignCenter)
            else:
                spacer = QWidget()
                spacer.setFixedSize(BTN_W, BTN_H)
                spacer.setStyleSheet('background:transparent; border:none;')
                hlay.addWidget(spacer, 0, Qt.AlignCenter)
            self.table.setCellWidget(r, 5, actions)
            self._action_holders[r] = (actions, bg_key)
            self._apply_row_widget_bg(r, hovered=False)
        self.table.resizeRowsToContents()
        self.table.resizeColumnsToContents()

    def _apply_search(self) -> None:
        self.refresh()

    def _add_invoice(self) -> None:
        client_id = self.client_id_provider()
        if not client_id:
            return
        dlg = DocumentCreateDialog(self.repo, int(client_id), "INVOICE", self)
        if dlg.exec() == QDialog.Accepted:
            self.refresh()

    def _add_invoice_all_unpaid(self) -> None:
        client_id = self.client_id_provider()
        if not client_id:
            return
        dlg = DocumentCreateDialog(self.repo, int(client_id), "INVOICE", self, select_all_unpaid=True, select_all_available=True)
        if dlg.exec() == QDialog.Accepted:
            self.refresh()

    def _add_act(self) -> None:
        client_id = self.client_id_provider()
        if not client_id:
            return
        dlg = DocumentCreateDialog(self.repo, int(client_id), "ACT", self)
        if dlg.exec() == QDialog.Accepted:
            self.refresh()

    def _add_act_all_unpaid(self) -> None:
        client_id = self.client_id_provider()
        if not client_id:
            return
        dlg = DocumentCreateDialog(self.repo, int(client_id), "ACT", self, select_all_available=True)
        if dlg.exec() == QDialog.Accepted:
            self.refresh()

    def _edit_template(self, doc_type: str) -> None:
        client_id = self.client_id_provider()
        if not client_id:
            return
        dlg = TemplateEditorDialog(self.repo, int(client_id), doc_type, self)
        dlg.exec()

    def _open_file(self, file_path: str) -> None:
        path = Path(file_path)
        if not path.exists():
            QMessageBox.warning(self, "Файл не знайдено", "Файл документа не знайдено на диску.")
            return
        QDesktopServices.openUrl(QUrl.fromLocalFile(str(path)))

    def _download_file(self, file_path: str) -> None:
        src = Path(file_path)
        if not src.exists():
            QMessageBox.warning(self, "Файл не знайдено", "Файл документа не знайдено на диску.")
            return
        dest, _ = QFileDialog.getSaveFileName(self, "Зберегти файл", src.name, "PDF Files (*.pdf)")
        if not dest:
            return
        shutil.copy2(src, dest)


    def _send_document_email(self, row: dict) -> None:
        client_id = self.client_id_provider()
        if not client_id:
            return
        client = self.repo.get_client(int(client_id))
        recipient = str(client['email'] or '').strip() if client else ''
        doc_type = str(row.get('doc_type') or '')
        type_label = 'рахунок' if doc_type == 'INVOICE' else 'акт'
        subject = f"{type_label.capitalize()} {row.get('doc_number') or ''}"
        body = self.repo.build_email_body(
            template_key='client_invoice' if doc_type == 'INVOICE' else 'client_act',
            doc_number=str(row.get('doc_number') or ''),
            recipient_name=str(client['display_name'] or client['company_name'] or '') if client else '',
        )
        dlg = EmailSendDialog(
            self.repo,
            client_id=int(client_id),
            document_id=int(row['id']),
            recipient_email=recipient,
            subject=subject,
            body=body,
            attachment_path=str(row.get('file_path') or ''),
            document_kind=doc_type,
            recipient_kind='client',
            recipient_name=str(client['display_name'] or client['company_name'] or '') if client else '',
            document_number=str(row.get('doc_number') or ''),
            parent=self,
        )
        if dlg.exec() == QDialog.Accepted:
            top = self.parent()
            while top is not None and not hasattr(top, 'email_history_tab'):
                top = top.parent()
            if top is not None and hasattr(top, 'email_history_tab'):
                try:
                    top.email_history_tab.refresh()
                except Exception:
                    pass

    def _delete_document_row(self, row: dict) -> None:
        doc_type = str(row.get('doc_type') or '')
        did = int(row['id'])
        file_path = str(row.get('file_path') or '')
        if doc_type == 'ACT':
            msg = QMessageBox(self)
            msg.setWindowTitle('Видалення акту')
            msg.setText('Оберіть спосіб видалення акту:')
            btn_both = msg.addButton('Видалити акт з усіма рахунками', QMessageBox.AcceptRole)
            btn_act = msg.addButton('Видалити акт без рахунків', QMessageBox.DestructiveRole)
            msg.addButton('Скасувати', QMessageBox.RejectRole)
            msg.exec()
            clicked = msg.clickedButton()
            if clicked == btn_both:
                removed = self.repo.delete_act_and_optionally_invoices(did, delete_invoices=True)
            elif clicked == btn_act:
                removed = self.repo.delete_act_and_optionally_invoices(did, delete_invoices=False)
            else:
                return
            for path_str in removed:
                try:
                    path = Path(path_str)
                    if path.exists():
                        path.unlink()
                except Exception:
                    pass
            self.refresh()
            return

        res = QMessageBox.question(self, 'Видалення', 'Видалити документ?', QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
        if res != QMessageBox.Yes:
            return
        try:
            path = Path(file_path)
            if path.exists():
                path.unlink()
        except Exception:
            pass
        self.repo.delete_document(did)
        self.refresh()

    def eventFilter(self, obj, event):
        if obj is self.table.viewport() and event.type() == QEvent.Leave:
            self._row_delegate.set_hover_row(-1)
            for row in list(self._action_holders):
                self._apply_row_widget_bg(row, hovered=False)
            self.table.viewport().update()
        return super().eventFilter(obj, event)

    def _on_hover_row(self, row: int, _col: int) -> None:
        self._row_delegate.set_hover_row(row)
        for r in list(self._action_holders):
            self._apply_row_widget_bg(r, hovered=(r == row))
        self.table.viewport().update()

    def _apply_row_widget_bg(self, row: int, hovered: bool) -> None:
        holder_info = self._action_holders.get(row)
        if not holder_info:
            return
        holder, bg_key = holder_info
        if bg_key == 'green':
            bg = '#D2F2DC'
        else:
            bg = '#FFF5CD'
        if hovered:
            bg = '#BFE7CC' if bg_key == 'green' else '#F6E4B0'
        holder.setStyleSheet(f'background:{bg}; border:none;')
