from __future__ import annotations

from html import escape
from pathlib import Path
import traceback

from PySide6.QtCore import QObject, QRunnable, Qt, Signal, Slot
from PySide6.QtGui import QStandardItem, QStandardItemModel
from PySide6.QtWidgets import (
    QComboBox, QFrame, QGridLayout, QHBoxLayout, QLabel, QPlainTextEdit,
    QPushButton, QTextBrowser, QVBoxLayout, QWidget,
)

from awakening_theme import GOLD_BRIGHT, SectionTitle
from image_repository import ImageRepository
from models import InventoryItem, level_label


class WorkerSignals(QObject):
    result = Signal(object)
    error = Signal(object)
    progress = Signal(object)
    finished = Signal()
    release = Signal(object)


class Worker(QRunnable):
    def __init__(self, fn, *args, **kwargs):
        super().__init__()
        # QThreadPool normally deletes QRunnable on the worker thread as soon as
        # run() returns.  PySide can then leave queued Python signal callbacks
        # referring to an already-destroyed wrapper.  MainWindow now keeps every
        # Worker alive and releases it on the GUI thread after ``finished``.
        self.setAutoDelete(False)
        self.fn = fn
        self.args = args
        self.kwargs = kwargs
        self.signals = WorkerSignals()

    @Slot()
    def run(self):
        try:
            result = self.fn(*self.args, progress=self.signals.progress.emit, **self.kwargs)
        except Exception as exc:  # noqa: BLE001
            try:
                exc._worker_traceback = traceback.format_exc()
            except Exception:
                pass
            self.signals.error.emit(exc)
        else:
            self.signals.result.emit(result)
        finally:
            self.signals.finished.emit()
            # MainWindow receives this through an explicit queued connection and
            # drops the final Python reference on the GUI thread.
            self.signals.release.emit(self)


class CheckableComboBox(QComboBox):
    """Compact multi-select combo box with checkmarks.

    ``selected_values()`` returns an empty list when the special "Wszystkie"
    option is active. This keeps filtering code simple while still allowing
    several categories, prefixes, bases, or suffixes to be selected together.
    """

    selectionChanged = Signal()

    def __init__(self, parent=None, *, allow_all: bool = True, all_label: str = "Wszystkie"):
        super().__init__(parent)
        self.allow_all = allow_all
        self.all_label = all_label
        self._model = QStandardItemModel(self)
        self.setModel(self._model)
        self.setEditable(True)
        self.lineEdit().setReadOnly(True)
        self.lineEdit().setPlaceholderText(all_label)
        self.view().pressed.connect(self._pressed)
        self.currentIndexChanged.connect(lambda _index: self._update_text())
        self._keep_popup_open = False
        self._updating = False

    @staticmethod
    def _make_item(text: str, checked: bool = False) -> QStandardItem:
        item = QStandardItem(text)
        item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsUserCheckable)
        item.setData(Qt.Checked if checked else Qt.Unchecked, Qt.CheckStateRole)
        return item

    def set_values(self, values: list[str], selected=None) -> None:
        previous = set(self.selected_values()) if selected is None else set(selected)
        was_all = self.is_all_selected() if selected is None else False
        self._updating = True
        self._model.clear()
        if self.allow_all:
            self._model.appendRow(self._make_item(self.all_label, checked=was_all or not previous))
        for value in values:
            self._model.appendRow(self._make_item(value, checked=value in previous))
        if self.allow_all and self._model.rowCount() and not self._checked_rows():
            self._model.item(0).setCheckState(Qt.Checked)
        if not self.allow_all and self._model.rowCount() and not self._checked_rows():
            self._model.item(0).setCheckState(Qt.Checked)
        self._updating = False
        self._update_text()

    def _checked_rows(self) -> list[int]:
        return [
            row for row in range(self._model.rowCount())
            if self._model.item(row).checkState() == Qt.Checked
        ]

    def is_all_selected(self) -> bool:
        return bool(
            self.allow_all
            and self._model.rowCount()
            and self._model.item(0).checkState() == Qt.Checked
        )

    def selected_values(self) -> list[str]:
        if self.is_all_selected():
            return []
        start = 1 if self.allow_all else 0
        return [
            self._model.item(row).text()
            for row in range(start, self._model.rowCount())
            if self._model.item(row).checkState() == Qt.Checked
        ]

    def set_selected_values(self, values) -> None:
        wanted = set(values)
        self._updating = True
        start = 1 if self.allow_all else 0
        if self.allow_all and self._model.rowCount():
            self._model.item(0).setCheckState(Qt.Checked if not wanted else Qt.Unchecked)
        for row in range(start, self._model.rowCount()):
            self._model.item(row).setCheckState(
                Qt.Checked if self._model.item(row).text() in wanted else Qt.Unchecked
            )
        if not self.allow_all and self._model.rowCount() and not self.selected_values():
            self._model.item(0).setCheckState(Qt.Checked)
        self._updating = False
        self._update_text()
        self.selectionChanged.emit()

    def select_all(self) -> None:
        if self.allow_all:
            self.set_selected_values([])
        elif self._model.rowCount():
            self.set_selected_values([self._model.item(0).text()])

    def _pressed(self, index) -> None:
        if self._updating or not index.isValid():
            return
        row = index.row()
        item = self._model.item(row)
        self._updating = True
        if self.allow_all and row == 0:
            new_state = Qt.Unchecked if item.checkState() == Qt.Checked else Qt.Checked
            item.setCheckState(new_state)
            if new_state == Qt.Checked:
                for other in range(1, self._model.rowCount()):
                    self._model.item(other).setCheckState(Qt.Unchecked)
        else:
            item.setCheckState(Qt.Unchecked if item.checkState() == Qt.Checked else Qt.Checked)
            if self.allow_all:
                self._model.item(0).setCheckState(Qt.Unchecked)
            if not self.selected_values():
                if self.allow_all:
                    self._model.item(0).setCheckState(Qt.Checked)
                else:
                    item.setCheckState(Qt.Checked)
        self._updating = False
        self._update_text()
        self.selectionChanged.emit()
        self._keep_popup_open = True

    def hidePopup(self) -> None:
        if self._keep_popup_open:
            self._keep_popup_open = False
            return
        super().hidePopup()

    def _update_text(self) -> None:
        if self.is_all_selected():
            text = self.all_label
        else:
            selected = self.selected_values()
            if not selected:
                text = self.all_label if self.allow_all else "Wybierz…"
            elif len(selected) <= 2:
                text = ", ".join(selected)
            else:
                text = f"{len(selected)} wybrane"
        self.lineEdit().setText(text)
        self.setToolTip(", ".join(self.selected_values()) or self.all_label)


class ImageTriptych(QWidget):
    def __init__(self, repository: ImageRepository, parent=None, *, main_size: int = 190, affix_size: int = 88):
        super().__init__(parent)
        self.repository = repository
        self.main_size = main_size
        self.affix_size = affix_size
        layout = QVBoxLayout(self)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(7)
        self.main = QLabel()
        self.main.setFixedSize(main_size, main_size)
        self.main.setAlignment(Qt.AlignCenter)
        self.main.setStyleSheet("background:#050505;border:2px solid #8b5a35;border-radius:3px;")
        layout.addWidget(self.main, alignment=Qt.AlignHCenter)
        affixes = QHBoxLayout()
        affixes.setSpacing(8)
        self.prefix = QLabel()
        self.suffix = QLabel()
        for label in (self.prefix, self.suffix):
            label.setFixedSize(affix_size, affix_size)
            label.setAlignment(Qt.AlignCenter)
            label.setStyleSheet("background:#070706;border:2px solid #8b5a35;border-radius:3px;")
            affixes.addWidget(label)
        layout.addLayout(affixes)
        self._paths = {"main": None, "prefix": None, "suffix": None}
        self._context = {
            "category": "", "base": "", "prefix_name": "", "suffix_name": "",
            "base_key": "", "prefix_key": "", "suffix_key": "",
            "quality": "", "legendary": False,
        }

    def set_images(
        self,
        *,
        main: str | Path | None = None,
        prefix: str | Path | None = None,
        suffix: str | Path | None = None,
        category: str = "",
        base: str = "",
        prefix_name: str = "",
        suffix_name: str = "",
        base_key: str = "",
        prefix_key: str = "",
        suffix_key: str = "",
        quality: str = "",
        legendary: bool = False,
    ) -> None:
        self._paths = {"main": main, "prefix": prefix, "suffix": suffix}
        self._context = {
            "category": category, "base": base, "prefix_name": prefix_name, "suffix_name": suffix_name,
            "base_key": base_key, "prefix_key": prefix_key, "suffix_key": suffix_key,
            "quality": quality, "legendary": legendary,
        }
        self._render()

    def update_paths(
        self,
        *,
        main: str | Path | None = None,
        prefix: str | Path | None = None,
        suffix: str | Path | None = None,
    ) -> None:
        """Replace only paths that were actually fetched.

        The semantic context and already displayed local affix icons stay intact
        when the official response does not contain a prefix/suffix URL.
        """
        if main:
            self._paths["main"] = main
        if prefix:
            self._paths["prefix"] = prefix
        if suffix:
            self._paths["suffix"] = suffix
        self._render()

    def _render(self) -> None:
        ctx = self._context
        main_fallback = self.repository.fallback_main(
            ctx["category"], ctx["base"], base_key=ctx["base_key"],
            quality=ctx["quality"], legendary=ctx["legendary"],
        )
        prefix_fallback = self.repository.fallback_prefix(
            ctx["prefix_name"], category=ctx["category"], prefix_key=ctx["prefix_key"],
        )
        suffix_fallback = self.repository.fallback_suffix(
            ctx["suffix_name"], category=ctx["category"], suffix_key=ctx["suffix_key"],
        )
        self.main.setPixmap(self.repository.pixmap(
            self._paths["main"], self.main_size - 6, self.main_size - 6, main_fallback,
        ))
        self.prefix.setPixmap(self.repository.pixmap(
            self._paths["prefix"], self.affix_size - 6, self.affix_size - 6, prefix_fallback,
        ))
        self.suffix.setPixmap(self.repository.pixmap(
            self._paths["suffix"], self.affix_size - 6, self.affix_size - 6, suffix_fallback,
        ))


def stats_html(row: dict | None, loading: str = "") -> str:
    if loading:
        return f"<div style='color:#d8c6aa;text-align:center;padding:12px'>{escape(loading)}</div>"
    if not row:
        return "<div style='color:#b9aa94;text-align:center;padding:12px'>Brak pobranych statystyk.</div>"
    parts: list[str] = []
    category = row.get("item_category") or ""
    if category:
        parts.append(f"<div style='color:#d9c7b0;font-style:italic;text-align:center;margin:3px'>{escape(str(category))}</div>")
    damage = row.get("damage")
    defense = row.get("defense")
    if damage:
        parts.append(f"<div style='text-align:center;font-weight:bold;color:#f0e7d6'>Obrażenia: <span style='color:#f3d45f'>{escape(str(damage))}</span></div>")
    if defense:
        parts.append(f"<div style='text-align:center;font-weight:bold;color:#f0e7d6'>Obrona: <span style='color:#f3d45f'>{escape(str(defense))}</span></div>")
    features = row.get("features")
    if features:
        pretty = escape(str(features)).replace(";", "<br>")
        parts.append(f"<div style='text-align:center;color:#62cef4;margin:9px 3px'>{pretty}</div>")
    requirements = row.get("requirements")
    if requirements:
        parts.append(f"<div style='text-align:center;color:#e7ded0;margin-top:10px'><b>Wymagania:</b> {escape(str(requirements))}</div>")
    prices = []
    if row.get("sell_price"):
        prices.append(f"Cena sprzedaży: {escape(str(row['sell_price']))}")
    elif row.get("price"):
        prices.append(f"Cena: {escape(str(row['price']))}")
    values = []
    if row.get("nanites") not in (None, ""):
        values.append(f"Nanity: {escape(str(row['nanites']))}")
    if row.get("mana") not in (None, ""):
        values.append(f"Mana: {escape(str(row['mana']))}")
    if prices:
        parts.append(f"<div style='text-align:center;color:#ded4c5;margin-top:12px'><b>{' • '.join(prices)}</b></div>")
    if values:
        parts.append(f"<div style='text-align:center;color:#cfc4b5'>{' • '.join(values)}</div>")
    return "".join(parts) or "<div style='color:#b9aa94;text-align:center'>Brak odczytanych statystyk.</div>"


class ItemCardOverlay(QFrame):
    """In-window item card. It is a child widget, not a separate OS window."""

    closed = Signal()

    def __init__(self, repository: ImageRepository, parent=None):
        super().__init__(parent)
        self.repository = repository
        self.setObjectName("itemCardOverlay")
        self.setStyleSheet(
            "QFrame#itemCardOverlay{background:rgba(2,2,2,247);border:2px solid #a4522d;border-radius:7px;}"
        )
        self.setMinimumSize(760, 430)
        self.resize(850, 470)
        self.hide()

        outer = QVBoxLayout(self)
        outer.setContentsMargins(9, 7, 9, 9)
        outer.setSpacing(5)

        title_row = QHBoxLayout()
        self.title_strip = SectionTitle("PRZEDMIOT")
        title_row.addWidget(self.title_strip, 1)
        self.close_button = QPushButton("X")
        self.close_button.setFixedSize(31, 29)
        self.close_button.setToolTip("Zamknij (Esc)")
        self.close_button.clicked.connect(self.close_card)
        title_row.addWidget(self.close_button)
        outer.addLayout(title_row)

        self.item_name = QLabel()
        self.item_name.setAlignment(Qt.AlignCenter)
        self.item_name.setWordWrap(True)
        self.item_name.setObjectName("itemTitle")
        outer.addWidget(self.item_name)

        body = QHBoxLayout()
        body.setSpacing(12)
        self.visual = ImageTriptych(repository, main_size=224, affix_size=100)
        body.addWidget(self.visual, 0, Qt.AlignTop)

        right = QVBoxLayout()
        self.stats_browser = QTextBrowser()
        self.stats_browser.setOpenExternalLinks(False)
        self.stats_browser.setFrameShape(QFrame.NoFrame)
        self.stats_browser.setStyleSheet("background:transparent;border:0;padding:0;")
        self.stats_browser.document().setDefaultStyleSheet("body{font-family:'Segoe UI';font-size:10pt;color:#e8dfd1}")
        right.addWidget(self.stats_browser, 1)

        self.metadata = QLabel()
        self.metadata.setWordWrap(True)
        self.metadata.setStyleSheet("color:#c9b591;border-top:1px solid #5e3d2c;padding-top:7px;")
        right.addWidget(self.metadata)

        self.status = QLabel()
        self.status.setWordWrap(True)
        self.status.setStyleSheet("color:#9ec9df;font-style:italic;")
        right.addWidget(self.status)
        body.addLayout(right, 1)
        outer.addLayout(body, 1)

        self._request_token = ""

    def close_card(self):
        self.hide()
        self.closed.emit()

    def keyPressEvent(self, event):
        if event.key() == Qt.Key_Escape:
            self.close_card()
            event.accept()
        else:
            super().keyPressEvent(event)

    def show_item(
        self,
        *,
        token: str,
        title: str,
        details: list[tuple[str, str]],
        main_image: str = "",
        prefix_image: str = "",
        suffix_image: str = "",
        category: str = "",
        base: str = "",
        prefix_name: str = "",
        suffix_name: str = "",
        base_key: str = "",
        prefix_key: str = "",
        suffix_key: str = "",
        legendary: bool = False,
        stats: dict | None = None,
        loading: str = "",
    ) -> None:
        self._request_token = token
        self.item_name.setObjectName("legendaryTitle" if legendary else "itemTitle")
        self.item_name.style().unpolish(self.item_name)
        self.item_name.style().polish(self.item_name)
        self.item_name.setText(title)
        quality = next((value for key, value in details if key == "Poziom"), "").split(" +", 1)[0]
        self.visual.set_images(
            main=main_image, prefix=prefix_image, suffix=suffix_image,
            category=category, base=base, prefix_name=prefix_name, suffix_name=suffix_name,
            base_key=base_key, prefix_key=prefix_key, suffix_key=suffix_key,
            quality=quality, legendary=legendary,
        )
        self.metadata.setText("  •  ".join(f"<b>{escape(k)}:</b> {escape(str(v or '—'))}" for k, v in details))
        self.stats_browser.setHtml(stats_html(stats, loading))
        self.status.setText(loading)
        self.show()
        self.raise_()
        self.setFocus(Qt.OtherFocusReason)

    def update_official(self, token: str, stats: dict, *, main: str = "", prefix: str = "", suffix: str = "", status: str = ""):
        if token != self._request_token or not self.isVisible():
            return
        if any((main, prefix, suffix)):
            self.visual.update_paths(main=main or None, prefix=prefix or None, suffix=suffix or None)
        self.stats_browser.setHtml(stats_html(stats))
        self.status.setText(status)

    def update_error(self, token: str, message: str):
        if token != self._request_token or not self.isVisible():
            return
        self.status.setText(f"Nie udało się pobrać statystyk: {message}")


def inventory_details(item: InventoryItem, rules) -> list[tuple[str, str]]:
    identity = item.identity
    return [
        ("Kategoria", rules.category_label(identity.category)),
        ("Legendarny", "TAK" if identity.legendary else "NIE"),
        ("Poziom", level_label(item.level)),
        ("Prefix", rules.prefix_filter_label(identity.prefix) if identity.prefix else "Brak"),
        ("Baza", rules.display(identity.base)),
        ("Suffix", rules.display(identity.suffix) if identity.suffix else "Brak"),
        ("Półka", item.shelf or "—"),
        ("Item ID", item.item_id or "—"),
    ]


def stats_text(row: dict) -> str:
    lines = []
    for label, key in [
        ("Obrażenia", "damage"), ("Obrona", "defense"), ("Cechy", "features"),
        ("Wymagania", "requirements"), ("Cena", "price"), ("Cena sprzedaży", "sell_price"),
        ("Mana", "mana"), ("Nanity", "nanites"),
        ("Strefa — przedmiot", "zone_item"), ("Strefa — prefix", "zone_prefix"),
        ("Strefa — suffix", "zone_suffix"), ("Strefa — razem", "zone_total"),
        ("Rzadkość — przedmiot", "rarity_item"), ("Rzadkość — prefix", "rarity_prefix"),
        ("Rzadkość — suffix", "rarity_suffix"), ("Rzadkość — razem", "rarity_total"),
    ]:
        value = row.get(key)
        if value not in (None, ""):
            lines.append(f"{label}: {value}")
    return "\n\n".join(lines) if lines else "Brak pobranych statystyk."
