from __future__ import annotations

from datetime import date
from pathlib import Path
import shutil
from typing import Any, Callable, Optional

from PySide6.QtCore import Qt, QTimer, QStringListModel
from PySide6.QtWidgets import (
    QAbstractSpinBox, QComboBox, QDateEdit, QDoubleSpinBox,
    QFormLayout, QFrame, QHBoxLayout, QLabel, QLineEdit, QMessageBox,
    QPushButton, QVBoxLayout, QWidget, QCompleter,
)

from mi100.repository import Repo
from mi100.services.project_folders import ensure_project_folder, open_folder


class SearchableComboBox(QComboBox):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setEditable(True)
        self.setInsertPolicy(QComboBox.NoInsert)
        self.setMaxVisibleItems(10)
        self._search_values: list[str] = []
        self._completer_model = QStringListModel(self)
        comp = QCompleter(self._completer_model, self)
        comp.setCaseSensitivity(Qt.CaseInsensitive)
        comp.setFilterMode(Qt.MatchContains)
        comp.setCompletionMode(QCompleter.PopupCompletion)
        self.setCompleter(comp)
        self.lineEdit().editingFinished.connect(self._sync_to_text)

    def clear(self):
        super().clear()
        self._search_values = []
        self._refresh_completer()

    def add_search_item(self, label: str, user_data=None, search_text: str = ''):
        super().addItem(label, userData=user_data)
        self._search_values.append((search_text or label or '').lower())
        self._refresh_completer()

    def _refresh_completer(self):
        values = []
        for i in range(self.count()):
            values.append(self.itemText(i))
        self._completer_model.setStringList(values)

    def _sync_to_text(self):
        txt = (self.currentText() or '').strip().lower()
        if not txt:
            return
        for i in range(self.count()):
            hay = f"{self.itemText(i)} {self._search_values[i] if i < len(self._search_values) else ''}".lower()
            if txt in hay:
                self.setCurrentIndex(i)
                return


class ActorRow(QWidget):
    def __init__(self, actors: list[dict[str, Any]], currencies: list[dict[str, Any]], on_change: Callable[[], None], parent=None):
        super().__init__(parent)
        self._on_change = on_change
        lay = QHBoxLayout(self)
        lay.setContentsMargins(0, 0, 0, 0)
        lay.setSpacing(8)

        self._currencies = currencies
        self.cmb_actor = SearchableComboBox()
        self.cmb_actor.setMinimumWidth(170)
        self.cmb_actor.setMaximumWidth(220)
        self.cmb_actor.lineEdit().setPlaceholderText("Пошук підрядника...")
        for a in actors:
            aid = int(a["id"])
            label = a.get("display_name") or f"ID {aid}"
            self.cmb_actor.add_search_item(str(label), user_data=aid, search_text=str(label))
        self.cmb_actor.setCurrentIndex(-1)
        self.cmb_actor.lineEdit().clear()
        lay.addWidget(self.cmb_actor, 0, Qt.AlignLeft)

        self.spin_fee = QDoubleSpinBox()
        self.spin_fee.setMinimumWidth(110)
        self.spin_fee.setMaximumWidth(160)
        self.spin_fee.setDecimals(2)
        self.spin_fee.setMaximum(1_000_000_000)
        self.spin_fee.setMinimum(0)
        self.spin_fee.setButtonSymbols(QAbstractSpinBox.NoButtons)
        self.spin_fee.lineEdit().setPlaceholderText("Гонорар")
        lay.addWidget(self.spin_fee, 0, Qt.AlignLeft)

        self.cmb_fee_currency = QComboBox()
        self.cmb_fee_currency.setMinimumWidth(90)
        self.cmb_fee_currency.setMaximumWidth(110)
        for cur in (self._currencies or []):
            self.cmb_fee_currency.addItem(f"{cur.get('symbol')} {cur.get('code')}", cur.get('code'))
        if self.cmb_fee_currency.count() == 0:
            self.cmb_fee_currency.addItem('₴ UAH', 'UAH')
        lay.addWidget(self.cmb_fee_currency, 0, Qt.AlignLeft)
        lay.addStretch(1)

        self.cmb_actor.currentIndexChanged.connect(self._on_change)
        self.spin_fee.valueChanged.connect(self._on_change)
        self.cmb_fee_currency.currentIndexChanged.connect(self._on_change)

    def set_data(self, row: dict[str, Any]) -> None:
        vid = row.get("voice_actor_id")
        for i in range(self.cmb_actor.count()):
            if self.cmb_actor.itemData(i) == vid:
                self.cmb_actor.setCurrentIndex(i)
                break
        self.spin_fee.setValue(float(row.get("fee", row.get("actor_fee", 0.0)) or 0.0))
        fee_cur = str(row.get("fee_currency") or row.get("currency") or "UAH")
        idx = self.cmb_fee_currency.findData(fee_cur)
        if idx < 0:
            self.cmb_fee_currency.addItem(fee_cur, fee_cur)
            idx = self.cmb_fee_currency.findData(fee_cur)
        self.cmb_fee_currency.setCurrentIndex(max(0, idx))

    def data(self) -> dict[str, Any]:
        vid = self.cmb_actor.currentData()
        return {
            "voice_actor_id": vid,
            "voice_actor_name_snapshot": self.cmb_actor.currentText().strip() if vid is not None else "",
            "fee": float(self.spin_fee.value()),
            "fee_currency": self.cmb_fee_currency.currentData() or self.cmb_fee_currency.currentText() or "UAH",
            "paid_to_actor": 0,
        }


class ProjectDetail(QWidget):
    def __init__(self, repo: Repo, project: Optional[dict[str, Any]], on_saved: Callable[[dict[str, Any]], None], on_deleted: Callable[[int], None], parent=None):
        super().__init__(parent)
        self.repo = repo
        self._project = project
        self._on_saved = on_saved
        self._on_deleted = on_deleted
        self._loading = False
        self._dirty = False
        self._all_actors: list[dict[str, Any]] = []
        self._actor_rows: list[ActorRow] = []
        self._autosave_timer = QTimer(self)
        self._autosave_timer.setSingleShot(True)
        self._autosave_timer.timeout.connect(self._autosave)
        self._build_ui()
        self._load()
        self._set_dirty(False)

    def _build_ui(self) -> None:
        root = QVBoxLayout(self)
        root.setContentsMargins(14, 14, 14, 14)
        root.setSpacing(12)

        self._title_label = QLabel("")
        self._title_label.hide()

        form_wrap = QFrame()
        form = QFormLayout(form_wrap)
        form.setLabelAlignment(Qt.AlignRight)
        form.setFormAlignment(Qt.AlignTop)
        form.setHorizontalSpacing(12)
        form.setFieldGrowthPolicy(QFormLayout.FieldsStayAtSizeHint)
        form.setVerticalSpacing(10)

        self.lbl_number = QLabel("—")
        self.btn_project_folder = QPushButton('Створити папку для проєкту')
        self.btn_project_folder.setMinimumHeight(26)
        self.btn_project_folder.setMinimumWidth(220)
        self.btn_project_folder.clicked.connect(self._handle_project_folder)
        self.date_created = QDateEdit(); self.date_created.setCalendarPopup(True); self.date_created.setMinimumWidth(170); self.date_created.setMaximumWidth(220)
        self.ed_title = QLineEdit(); self.ed_title.setMinimumWidth(170); self.ed_title.setMaximumWidth(220); self.ed_title.textChanged.connect(self._sync_title)
        self.cmb_client = SearchableComboBox(); self.cmb_client.setMinimumWidth(170); self.cmb_client.setMaximumWidth(220); self.cmb_client.lineEdit().setPlaceholderText("Пошук замовника...")
        self.spin_total = QDoubleSpinBox(); self.spin_total.setMinimumWidth(170); self.spin_total.setMaximumWidth(220); self.spin_total.setDecimals(2); self.spin_total.setMaximum(1_000_000_000); self.spin_total.setMinimum(0); self.spin_total.setButtonSymbols(QAbstractSpinBox.NoButtons)
        self.cmb_currency = QComboBox(); self.cmb_currency.setMinimumWidth(170); self.cmb_currency.setMaximumWidth(220);
        for cur in self.repo.get_selectable_currencies():
            self.cmb_currency.addItem(f"{cur.get('symbol')} {cur.get('code')}", cur.get('code'))

        number_row = QWidget()
        number_row_l = QHBoxLayout(number_row)
        number_row_l.setContentsMargins(0,0,0,0)
        number_row_l.setSpacing(8)
        number_row_l.addWidget(self.lbl_number, 0, Qt.AlignLeft)
        number_row_l.addWidget(self.btn_project_folder, 0, Qt.AlignLeft)
        number_row_l.addStretch(1)
        form.addRow(self._form_label("Номер"), number_row)
        form.addRow(self._form_label("Дата"), self.date_created)
        form.addRow(self._form_label("Назва"), self.ed_title)
        form.addRow(self._form_label("Замовник"), self.cmb_client)
        total_row = QWidget()
        total_row_l = QHBoxLayout(total_row)
        total_row_l.setContentsMargins(0,0,0,0)
        total_row_l.setSpacing(8)
        total_row_l.addWidget(self.spin_total, 0, Qt.AlignLeft)
        total_row_l.addWidget(self.cmb_currency, 0, Qt.AlignLeft)
        total_row_l.addStretch(1)
        form.addRow(self._form_label("Вартість"), total_row)

        self.actor_box = QFrame()
        self.actor_layout = QVBoxLayout(self.actor_box)
        self.actor_layout.setContentsMargins(0, 0, 0, 0)
        self.actor_layout.setSpacing(6)
        form.addRow(self._form_label("Підрядники"), self.actor_box)

        root.addWidget(form_wrap, 1)

        actions = QHBoxLayout()
        actions.addStretch(1)
        self.btn_delete = QPushButton("Видалити проєкт")
        self.btn_delete.setToolTip("Видалити проєкт")
        self.btn_delete.setObjectName("Danger")
        self.btn_delete.setMinimumHeight(30)
        self.btn_delete.setStyleSheet("QPushButton{background:#FFA8A8;color:#7C2D12;border:1px solid #E59494;border-radius:4px;padding:4px 10px;font-weight:600;} QPushButton:hover{background:#E59494;}")
        self.btn_delete.clicked.connect(self._delete)
        self.btn_delete.setVisible(self._project is not None)
        actions.addWidget(self.btn_delete)
        root.addLayout(actions)

        hint = QLabel("Підказка: кількість підрядників у проєкті необмежена. Гонорари підрядників не впливають на вартість проєкту для замовника.")
        hint.setObjectName("muted")
        hint.setWordWrap(True)
        root.addWidget(hint)

        self._wire_dirty_signals()

    def _form_label(self, text: str) -> QLabel:
        lbl = QLabel(f"{text}:")
        lbl.setMinimumWidth(130)
        lbl.setStyleSheet("font-weight:700; color:#1F2937; background:transparent; border:none; padding:0; margin:0;")
        return lbl

    def _sync_title(self, *_args) -> None:
        self._title_label.setText((self.ed_title.text() or "").strip())

    def _wire_dirty_signals(self) -> None:
        def hook(widget, signal_name: str) -> None:
            sig = getattr(widget, signal_name, None)
            if sig is not None:
                sig.connect(self._on_any_change)
        for w, s in [
            (self.ed_title, 'textChanged'), (self.cmb_client, 'currentIndexChanged'), (self.date_created, 'dateChanged'),
            (self.spin_total, 'valueChanged'), (self.cmb_currency, 'currentIndexChanged')]:
            hook(w, s)

    def _on_any_change(self, *args) -> None:
        if self._loading:
            return
        self._ensure_trailing_blank_actor_row()
        self._update_project_folder_button()
        self._set_dirty(True)

    def _set_dirty(self, is_dirty: bool) -> None:
        self._dirty = bool(is_dirty)
        if self._dirty and not self._loading:
            self._autosave_timer.start(650)

    def _autosave(self) -> None:
        if self._loading or not self._dirty:
            return
        title = (self.ed_title.text() or '').strip()
        client_id = self.cmb_client.currentData()
        if not title or client_id is None:
            return
        self._save(False, True)

    def _clear_actor_rows(self):
        while self._actor_rows:
            w = self._actor_rows.pop()
            w.setParent(None)
            w.deleteLater()

    def _add_actor_row(self, data: Optional[dict[str, Any]] = None) -> ActorRow:
        current_codes = [str((data or {}).get('fee_currency') or '').strip()] if data else []
        row = ActorRow(self._all_actors, self.repo.get_selectable_currencies(current_codes), self._on_any_change)
        if data:
            row.set_data(data)
        self.actor_layout.addWidget(row)
        self._actor_rows.append(row)
        return row

    def _ensure_trailing_blank_actor_row(self):
        if not self._actor_rows:
            self._add_actor_row()
            return
        last = self._actor_rows[-1]
        if last.cmb_actor.currentData() is not None:
            self._add_actor_row()

    def _load(self) -> None:
        self._loading = True
        self._load_clients(); self._load_actors_master(); self._clear_actor_rows()
        if self._project is None:
            self.lbl_number.setText(str(self.repo.get_next_project_number()))
            self._title_label.setText("")
            self.date_created.setDate(date.today())
            self.cmb_client.setCurrentIndex(-1)
            self.cmb_client.lineEdit().clear()
            self.spin_total.setValue(0.0)
            self.cmb_currency.clear()
            for cur in self.repo.get_selectable_currencies(['UAH']):
                self.cmb_currency.addItem(f"{cur.get('symbol')} {cur.get('code')}", cur.get('code'))
            idx = self.cmb_currency.findData('UAH')
            self.cmb_currency.setCurrentIndex(max(0, idx))
            self._add_actor_row(); self._ensure_trailing_blank_actor_row(); self._loading = False; self._update_project_folder_button(); self._set_dirty(False); return
        p = self._project
        self.lbl_number.setText(str(p.get('project_number') or '—'))
        try:
            y, m, d = (int(x) for x in str(p.get('created_date') or '').split('-')[:3]); self.date_created.setDate(date(y, m, d))
        except Exception:
            self.date_created.setDate(date.today())
        self.ed_title.setText(str(p.get('title') or ''))
        self._title_label.setText(str(p.get('title') or ''))
        self._select_by_user_data(self.cmb_client, p.get('client_id'))
        self.spin_total.setValue(float(p.get('total_amount') or 0.0))
        cur_code = str(p.get('currency') or 'UAH')
        self.cmb_currency.clear()
        for cur in self.repo.get_selectable_currencies([cur_code]):
            self.cmb_currency.addItem(f"{cur.get('symbol')} {cur.get('code')}", cur.get('code'))
        idx = self.cmb_currency.findData(cur_code)
        if idx < 0:
            self.cmb_currency.addItem(cur_code, cur_code); idx = self.cmb_currency.findData(cur_code)
        self.cmb_currency.setCurrentIndex(max(0, idx))
        actor_rows = p.get('actor_rows') or []
        if actor_rows:
            for r in actor_rows:
                self._add_actor_row(r)
        else:
            self._add_actor_row()
        self._ensure_trailing_blank_actor_row()
        self._loading = False; self._update_project_folder_button(); self._set_dirty(False)

    def _can_enable_project_folder(self) -> bool:
        title = (self.ed_title.text() or '').strip()
        client_id = self.cmb_client.currentData()
        created_date = self.date_created.date().toString('yyyy-MM-dd')
        return bool(self._project and title and client_id is not None and created_date)

    def _update_project_folder_button(self) -> None:
        path = str((self._project or {}).get('project_folder_path') or '')
        exists = bool(path and Path(path).exists())
        enabled = self._can_enable_project_folder()
        self.btn_project_folder.setEnabled(enabled)
        if exists:
            self.btn_project_folder.setText('Відкрити папку проєкту')
            self.btn_project_folder.setStyleSheet("QPushButton{background:#FFD8A8;color:#8E6129;border:1px solid #E5BF90;border-radius:6px;padding:2px 10px;font-weight:600;}QPushButton:hover{background:#E5BF90;}QPushButton:disabled{background:#EBEBEB;color:#6B7280;border:1px solid #D7D7D7;}")
        elif enabled:
            self.btn_project_folder.setText('Створити папку для проєкту')
            self.btn_project_folder.setStyleSheet("QPushButton{background:#4FAF7A;color:#FFFFFF;border:1px solid #459E6E;border-radius:6px;padding:2px 10px;font-weight:600;}QPushButton:hover{background:#459E6E;}QPushButton:disabled{background:#EBEBEB;color:#6B7280;border:1px solid #D7D7D7;}")
        else:
            self.btn_project_folder.setText('Створити папку для проєкту')
            self.btn_project_folder.setStyleSheet("QPushButton{background:#EBEBEB;color:#6B7280;border:1px solid #D7D7D7;border-radius:6px;padding:2px 10px;font-weight:600;}")

    def _handle_project_folder(self) -> None:
        if not self._can_enable_project_folder():
            return
        if self._dirty:
            self._save(False, True)
        if not self._project:
            return
        existing_path = str(self._project.get('project_folder_path') or '')
        if existing_path and Path(existing_path).exists():
            open_folder(existing_path)
            return
        folder_path = ensure_project_folder(
            project_title=(self.ed_title.text() or '').strip(),
            company_name=(self.cmb_client.currentText() or '').strip(),
            created_date=self.date_created.date().toString('yyyy-MM-dd'),
            existing_path=existing_path,
        )
        self.repo.set_project_folder_path(int(self._project['id']), folder_path)
        self._project = self.repo.get_project(int(self._project['id']))
        self._update_project_folder_button()
        open_folder(folder_path)

    def _load_clients(self) -> None:
        self.cmb_client.clear()
        for c in self.repo.list_clients():
            c = dict(c)
            cid = int(c['id'])
            company = c.get('company_name') or c.get('display_name') or f'ID {cid}'
            contact = (c.get('responsible_person_name') or '').strip()
            label = str(company)
            search_text = f"{company} {contact} {c.get('display_name') or ''}"
            self.cmb_client.add_search_item(label, user_data=cid, search_text=search_text)

    def _load_actors_master(self) -> None:
        self._all_actors = [dict(a) for a in self.repo.list_voice_actors()]

    def _collect_actor_rows(self) -> list[dict[str, Any]]:
        rows = []
        for r in self._actor_rows:
            d = r.data()
            if d['voice_actor_id'] is not None:
                rows.append(d)
        return rows

    def _save(self, show_message: bool = False, silent: bool = False) -> None:
        title = (self.ed_title.text() or '').strip(); client_id = self.cmb_client.currentData(); work_status = 'IN_PROGRESS' if self._project is None else (self._project.get('work_status') or 'IN_PROGRESS'); client_paid = int((self._project or {}).get('client_paid') or 0); total_amount = float(self.spin_total.value()); currency = self.cmb_currency.currentData() or self.cmb_currency.currentText() or 'UAH'; created_date = self.date_created.date().toString('yyyy-MM-dd'); actor_rows = self._collect_actor_rows()
        if not title:
            if not silent: QMessageBox.warning(self, 'Помилка', 'Вкажіть назву проєкту.')
            return
        if client_id is None:
            if not silent: QMessageBox.warning(self, 'Помилка', 'Оберіть замовника.')
            return
        if self._project is None:
            pid = self.repo.create_project(title=title, client_id=int(client_id), total_amount=total_amount, currency=currency, created_date=created_date, work_status=str(work_status), client_paid=int(client_paid), actor_rows=actor_rows)
            self._project = self.repo.get_project(pid)
            if self._project:
                self.btn_delete.setVisible(True); self._on_saved(self._project); self.lbl_number.setText(str(self._project.get('project_number') or self.lbl_number.text()))
                self._update_project_folder_button()
                pass  # без зайвого інформаційного вікна
                self._set_dirty(False)
            return
        pid = int(self._project['id'])
        self.repo.update_project(project_id=pid, title=title, client_id=int(client_id), total_amount=total_amount, currency=currency, created_date=created_date, work_status=str(work_status), client_paid=int(client_paid), actor_rows=actor_rows)
        self._project = self.repo.get_project(pid)
        if self._project: self._on_saved(self._project)
        self._update_project_folder_button()
        pass  # без зайвого інформаційного вікна
        self._set_dirty(False)


    def _confirm_project_delete_options(self) -> tuple[bool, bool]:
        dlg = QDialog(self)
        dlg.setWindowTitle('Видалення проєкту')
        lay = QVBoxLayout(dlg)
        lay.addWidget(QLabel('Оберіть варіант видалення:'))
        chk_with = QCheckBox('Видалити проєкт і папку з файлами проєкту')
        chk_keep = QCheckBox('Видалити проєкт зі списку, але залишити всі файли')
        chk_with.setChecked(True)
        def sync_from_with(state):
            if state:
                chk_keep.setChecked(False)
            elif not chk_keep.isChecked():
                chk_keep.setChecked(True)
        def sync_from_keep(state):
            if state:
                chk_with.setChecked(False)
            elif not chk_with.isChecked():
                chk_with.setChecked(True)
        chk_with.stateChanged.connect(sync_from_with)
        chk_keep.stateChanged.connect(sync_from_keep)
        lay.addWidget(chk_with)
        lay.addWidget(chk_keep)
        btns = QHBoxLayout(); btns.addStretch(1)
        btn_yes = QPushButton('Видалити'); btn_no = QPushButton('Скасувати')
        btn_yes.clicked.connect(dlg.accept); btn_no.clicked.connect(dlg.reject)
        btns.addWidget(btn_yes); btns.addWidget(btn_no)
        lay.addLayout(btns)
        if dlg.exec() != QDialog.Accepted:
            return False, False
        return True, bool(chk_with.isChecked())

    def _delete(self) -> None:
        if self._project is None:
            return
        pid = int(self._project['id'])
        ok, delete_folder = self._confirm_project_delete_options()
        if not ok:
            return
        folder_path = str(self._project.get('project_folder_path') or '')
        self.repo.delete_project(pid)
        if delete_folder and folder_path:
            try:
                p = Path(folder_path)
                if p.exists():
                    shutil.rmtree(p, ignore_errors=True)
            except Exception:
                pass
        self._on_deleted(pid)

    @staticmethod
    def _select_by_user_data(combo: QComboBox, value: Any) -> None:
        for i in range(combo.count()):
            if combo.itemData(i) == value:
                combo.setCurrentIndex(i); return
