import os
import sys
import qtawesome as qta
from PyQt6.QtWidgets import (QWidget, QLabel, QVBoxLayout, QSizePolicy,
                              QMenu, QFileIconProvider, QMessageBox)
from PyQt6.QtGui import (
    QDrag, QPixmap, QPainter, QColor, QIcon, QPen, QKeySequence,
    QFont, QFontMetrics, QTextLayout, QTextOption,
)
from PyQt6.QtCore import Qt, QMimeData, QByteArray, QSize, QPoint, QFileInfo, QPropertyAnimation, QEasingCurve
from db import database
import ui.theme as theme
import ui.dialog_style as dialog_style
import shortcut_target

_icon_provider = QFileIconProvider()


def _wrapped_line_count(text: str, font: QFont, width_px: int) -> int:
    if width_px < 4 or not text:
        return 0 if not text else 999
    tl = QTextLayout(text, font)
    opt = QTextOption()
    opt.setWrapMode(QTextOption.WrapMode.WrapAtWordBoundaryOrAnywhere)
    tl.setTextOption(opt)
    tl.beginLayout()
    n = 0
    while True:
        line = tl.createLine()
        if not line.isValid():
            break
        line.setLineWidth(width_px)
        n += 1
    tl.endLayout()
    return n


def two_line_display_text(text: str, font: QFont, width_px: int) -> str:
    """最多按宽度折成 2 行；超出部分用省略号。"""
    if not text:
        return text
    if _wrapped_line_count(text, font, width_px) <= 2:
        return text
    elide = "…"
    lo, hi = 0, len(text)
    best = elide
    while lo <= hi:
        mid = (lo + hi) // 2
        cand = text[:mid].rstrip() + elide
        if _wrapped_line_count(cand, font, width_px) <= 2:
            best = cand
            lo = mid + 1
        else:
            hi = mid - 1
    return best


def extract_icon(path: str) -> QIcon:
    try:
        icon = _icon_provider.icon(QFileInfo(path))
        if not icon.isNull():
            px = icon.pixmap(QSize(36, 36))
            if not px.isNull() and px.width() > 4:
                return icon
    except Exception:
        pass
    ext = os.path.splitext(path)[1].lower()
    color = "#4a9eff"
    if ext == ".exe":   return qta.icon("fa5s.desktop",  color=color)
    if ext == ".lnk":   return qta.icon("fa5s.link",     color=color)
    if ext in (".bat", ".cmd"): return qta.icon("fa5s.terminal", color=color)
    if ext == ".url":   return qta.icon("fa5s.globe",    color=color)
    return qta.icon("fa5s.file", color=color)


class ItemWidget(QWidget):
    DRAG_THRESHOLD = 8

    def __init__(self, item_data: dict, parent=None):
        super().__init__(parent)
        self.item_data = item_data
        self.setFixedSize(68, 76)
        self.setCursor(Qt.CursorShape.PointingHandCursor)
        self._drag_start_pos = QPoint()
        self._selected = False
        self._build_ui()

    def showEvent(self, event):
        super().showEvent(event)
        self._update_name_display()

    def resizeEvent(self, event):
        super().resizeEvent(event)
        self._update_name_display()

    def _build_ui(self):
        layout = QVBoxLayout(self)
        layout.setContentsMargins(4, 6, 4, 4)
        layout.setSpacing(3)

        icon = extract_icon(self.item_data["path"])
        self.icon_label = QLabel()
        self.icon_label.setPixmap(icon.pixmap(QSize(36, 36)))
        self.icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        self.icon_label.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents)

        self.name_label = QLabel()
        self.name_label.setAlignment(Qt.AlignmentFlag.AlignCenter | Qt.AlignmentFlag.AlignTop)
        self.name_label.setWordWrap(True)
        self.name_label.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
        self.name_label.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents)
        self._apply_theme()

        layout.addWidget(self.icon_label)
        layout.addWidget(self.name_label)

    def _update_name_display(self):
        name = self.item_data.get("name") or ""
        w = max(8, self.width() - 8)
        font = self.name_label.font()
        disp = two_line_display_text(name, font, w)
        self.name_label.setText(disp)
        path = self.item_data.get("path") or ""
        self.setToolTip(f"{name}\n{path}" if path else name)

    def _apply_theme(self):
        t = theme.current()
        self.name_label.setStyleSheet(f"font-size:10px; color:{t['item_name_color']};")
        fm = QFontMetrics(self.name_label.font())
        self.name_label.setMaximumHeight(int(fm.lineSpacing() * 2 + 4))
        self._update_name_display()

    def matches(self, keyword: str) -> bool:
        return keyword.lower() in self.item_data["name"].lower()

    # ── 选中高亮 ─────────────────────────────────────────
    def _set_selected(self, val: bool):
        self._selected = val
        self.update()

    def paintEvent(self, event):
        if self._selected:
            p = QPainter(self)
            p.setRenderHint(QPainter.RenderHint.Antialiasing)
            p.setBrush(QColor(74, 158, 255, 50))
            p.setPen(QPen(QColor("#4a9eff"), 1.5))
            p.drawRoundedRect(1, 1, self.width() - 2, self.height() - 2, 6, 6)
            p.end()
        super().paintEvent(event)

    # ── 鼠标事件 ─────────────────────────────────────────
    def mouseDoubleClickEvent(self, event):
        if event.button() == Qt.MouseButton.LeftButton:
            self._launch()

    def mousePressEvent(self, event):
        if event.button() == Qt.MouseButton.LeftButton:
            self._drag_start_pos = event.pos()
            fc = self.parent()
            if fc and hasattr(fc, "_clear_selection_except"):
                fc._clear_selection_except(self)
            self._set_selected(True)
        super().mousePressEvent(event)

    def mouseReleaseEvent(self, event):
        super().mouseReleaseEvent(event)

    def mouseMoveEvent(self, event):
        if not (event.buttons() & Qt.MouseButton.LeftButton):
            return
        if (event.pos() - self._drag_start_pos).manhattanLength() < self.DRAG_THRESHOLD:
            return
        self._set_selected(False)
        drag = QDrag(self)
        mime = QMimeData()
        item_id = self.item_data.get("id")
        id_bytes = str(item_id).encode() if item_id is not None else b"none"
        mime.setData("application/x-item-id", QByteArray(id_bytes))
        # 也携带文件路径，方便跨组拖拽文件夹分组的 item
        from PyQt6.QtCore import QUrl
        mime.setUrls([QUrl.fromLocalFile(self.item_data["path"])])
        drag.setMimeData(mime)

        src = self.grab()
        scale = 1.4
        pw, ph = int(src.width() * scale), int(src.height() * scale)
        preview = QPixmap(pw, ph)
        preview.fill(QColor(0, 0, 0, 0))
        painter = QPainter(preview)
        painter.setRenderHint(QPainter.RenderHint.Antialiasing)
        painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform)
        painter.setBrush(QColor(60, 60, 60, 160))
        painter.setPen(Qt.PenStyle.NoPen)
        painter.drawRoundedRect(0, 0, pw, ph, 8, 8)
        painter.setOpacity(0.92)
        painter.drawPixmap(0, 0, pw, ph, src)
        painter.end()
        drag.setPixmap(preview)
        drag.setHotSpot(QPoint(int(event.pos().x() * scale), int(event.pos().y() * scale)))
        drag.exec(Qt.DropAction.MoveAction)

    def contextMenuEvent(self, event):
        fc = self.parent()
        if fc and hasattr(fc, "_clear_selection_except"):
            if not self._selected:
                fc._clear_selection_except(self)
                self._set_selected(True)
        else:
            self._set_selected(True)

        t = theme.current()
        menu_style = f"""
            QMenu {{ background:{t['menu_bg']}; color:{t['menu_color']};
                     border:1px solid {t['menu_border']}; padding:4px; border-radius:6px; }}
            QMenu::item {{ padding:6px 18px; border-radius:3px; }}
            QMenu::item:selected {{ background:{t['menu_selected']}; }}
        """
        menu = QMenu(self)
        menu.setStyleSheet(menu_style)

        open_act = menu.addAction(qta.icon("fa5s.play", color="#4a9eff"), "打开")
        menu.addSeparator()

        item_id = self.item_data.get("id")
        is_folder_item = item_id is None

        rename_act = menu.addAction(qta.icon("fa5s.pen", color="#aaa"), "重命名")
        if not is_folder_item:
            repath_act = menu.addAction(qta.icon("fa5s.folder-open", color="#aaa"), "更换路径")
        else:
            repath_act = None
        menu.addSeparator()
        del_act = menu.addAction(qta.icon("fa5s.trash", color="#e55"), "删除")

        # 菜单弹出时单键触发：O 打开 / M 重命名 / D 删除
        _ctx = Qt.ShortcutContext.WidgetWithChildrenShortcut
        open_act.setShortcut(QKeySequence(Qt.Key.Key_O))
        open_act.setShortcutContext(_ctx)
        rename_act.setShortcut(QKeySequence(Qt.Key.Key_M))
        rename_act.setShortcutContext(_ctx)
        del_act.setShortcut(QKeySequence(Qt.Key.Key_D))
        del_act.setShortcutContext(_ctx)

        action = menu.exec(event.globalPos())

        if action == open_act:
            self._launch()
        elif action == rename_act:
            self._rename()
        elif repath_act and action == repath_act:
            self._change_path()
        elif action == del_act:
            self._delete_item()

    # ── 操作 ─────────────────────────────────────────────
    def _rename(self):
        is_folder_item = self.item_data.get("id") is None
        old_path = self.item_data["path"]
        ext = os.path.splitext(old_path)[1]

        # 文件夹分组显示带后缀的完整文件名，普通分组只显示名称
        if is_folder_item:
            current_display = self.item_data["name"] + ext
        else:
            current_display = self.item_data["name"]

        name, ok = dialog_style.get_text(self, "重命名", "新名称:", text=current_display)
        if not ok or not name.strip() or name.strip() == current_display:
            return
        new_input = name.strip()

        if is_folder_item:
            # 用户输入的就是完整文件名（含后缀）
            new_filename = new_input
            new_name = os.path.splitext(new_filename)[0] or new_filename
            new_path = os.path.join(os.path.dirname(old_path), new_filename)
            try:
                os.rename(old_path, new_path)
                self.item_data["path"] = new_path
                self.item_data["name"] = new_name
                self._update_name_display()
            except Exception as e:
                dialog_style.warning(self, "重命名失败", str(e))
        else:
            conn = database.get_conn()
            conn.execute("UPDATE items SET name=? WHERE id=?",
                         (new_input, self.item_data["id"]))
            conn.commit()
            conn.close()
            self.item_data["name"] = new_input
            self._update_name_display()

    def _change_path(self):
        path, _ = dialog_style.get_open_file_name(
            self,
            "选择新路径",
            os.path.dirname(self.item_data["path"]),
            "程序/快捷方式 (*.exe *.lnk *.bat *.cmd *.url);;所有文件 (*)",
        )
        if path:
            store = shortcut_target.path_for_storage(path)
            conn = database.get_conn()
            conn.execute("UPDATE items SET path=? WHERE id=?",
                         (store, self.item_data["id"]))
            conn.commit()
            conn.close()
            self.item_data["path"] = store
            self.icon_label.setPixmap(extract_icon(store).pixmap(QSize(36, 36)))
            self._update_name_display()

    def _delete_item(self, skip_confirm: bool = False, skip_refresh: bool = False):
        item_path = self.item_data["path"]
        item_id = self.item_data.get("id")
        is_folder_item = item_id is None

        if is_folder_item and os.path.isfile(item_path):
            if not skip_confirm:
                ret = dialog_style.question(
                    self,
                    "删除确认",
                    f"确认删除文件？\n\n{item_path}",
                    buttons=QMessageBox.StandardButton.Yes
                    | QMessageBox.StandardButton.Cancel,
                    default_button=QMessageBox.StandardButton.Cancel,
                )
                if ret != QMessageBox.StandardButton.Yes:
                    return
            try:
                try:
                    import send2trash
                    send2trash.send2trash(item_path)
                except ImportError:
                    os.remove(item_path)
            except Exception as e:
                dialog_style.warning(self, "删除失败", str(e))
                return
        else:
            if not skip_confirm:
                ret = dialog_style.question(
                    self,
                    "确认",
                    f"从分组中移除「{self.item_data['name']}」？",
                    default_button=QMessageBox.StandardButton.No,
                )
                if ret != QMessageBox.StandardButton.Yes:
                    return
            if item_id is not None:
                database.delete_item(item_id)

        if skip_refresh:
            return
        p = self.parent()
        while p and not hasattr(p, "refresh"):
            p = p.parent()
        if p:
            p.refresh()

    def _launch(self):
        path = (self.item_data.get("path") or "").strip()
        if not path:
            return
        lp = path.lower()
        if lp.startswith(("http://", "https://")):
            import webbrowser
            webbrowser.open(path)
            return

        if sys.platform == "win32":
            path = os.path.normpath(path)

        try:
            if sys.platform == "win32":
                os.startfile(path)
            else:
                import subprocess
                subprocess.Popen(["xdg-open", path])
        except OSError:
            try:
                import subprocess
                if sys.platform == "win32":
                    # shell=True 无法直接“运行” .lnk，需交给 start 做关联打开
                    subprocess.Popen(
                        ["cmd", "/c", "start", "", path],
                        creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
                    )
                else:
                    subprocess.Popen(["xdg-open", path])
            except Exception as e:
                print(f"启动失败: {path!r} -> {e}")
