from pprint import pformat
from typing import Literal

from conan_unified_api.types import ConanPkg
from dictdiffer import diff
from PySide6.QtCore import QRegularExpression, Qt
from PySide6.QtGui import QAction, QColor, QShowEvent, QTextCharFormat
from PySide6.QtWidgets import QDialog, QListWidgetItem, QMenu
from typing_extensions import override

from conan_explorer.app.logger import Logger
from conan_explorer.ui.common.syntax_highlighting import ConfigHighlighter
from conan_explorer.ui.common.theming import ThemedWidget


class ConfigDiffHighlighter(ConfigHighlighter):
    """Syntax highlighter to highlight the differences of a dict with different
    background colors (modified, added, removed)"""

    DIFF_NEW_COLOR = QColor("green")
    DIFF_REMOVED_COLOR = QColor("red")
    DIFF_MODIFIED_COLOR = QColor("orange")

    def __init__(self, parent, type: Literal["ini", "yaml"]) -> None:
        super().__init__(parent, type)
        self._reset_diff()

    def _reset_diff(self):
        self.modified_diffs = []
        self.added_diffs = []
        self.removed_diffs = []

    @override
    def highlightBlock(self, text: str):
        super().highlightBlock(text)
        key_format = QTextCharFormat()
        # clear all background color
        key_format.clearBackground()
        expression = QRegularExpression(".*")
        match = expression.match(text)
        self.setFormat(match.capturedStart(), match.capturedLength(), key_format)

        for diff_item in self.modified_diffs:
            key_format.setBackground(self.DIFF_MODIFIED_COLOR)
            expression = QRegularExpression(diff_item)
            match = expression.match(text)
            self.setFormat(match.capturedStart(), match.capturedLength(), key_format)
        for diff_item in self.added_diffs:
            key_format.setBackground(self.DIFF_NEW_COLOR)
            expression = QRegularExpression(diff_item)
            match = expression.match(text)
            self.setFormat(match.capturedStart(), match.capturedLength(), key_format)
        for diff_item in self.removed_diffs:
            key_format.setBackground(self.DIFF_REMOVED_COLOR)
            expression = QRegularExpression(diff_item)
            match = expression.match(text)
            self.setFormat(match.capturedStart(), match.capturedLength(), key_format)


class PkgDiffDialog(QDialog, ThemedWidget):
    def __init__(self, parent) -> None:
        QDialog.__init__(self, parent)
        ThemedWidget.__init__(self, None)
        from .diff_ui import Ui_Dialog

        self._ui = Ui_Dialog()
        self._ui.setupUi(self)
        self._init_pkg_context_menu()

        self.setWindowTitle("Compare Packages")
        self.setWindowFlag(Qt.WindowType.WindowMaximizeButtonHint)

        self._left_content = {}
        self._right_content = {}
        self._item_data = []

        self._left_highlighter = ConfigDiffHighlighter(
            self._ui.left_text_browser.document(), "yaml"
        )
        self._right_highlighter = ConfigDiffHighlighter(
            self._ui.right_text_browser.document(), "yaml"
        )
        self._ui.button_box.accepted.connect(self.close)

        self._ui.pkgs_list_widget.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
        self._ui.pkgs_list_widget.customContextMenuRequested.connect(
            self.on_pkg_context_menu_requested
        )

        # set up changed left element connection
        self._ui.pkgs_list_widget.currentItemChanged.connect(self._on_item_changed)

    def _init_pkg_context_menu(self):
        """Initalize context menu with all actions"""
        self.select_cntx_menu = QMenu()

        self.set_ref_item = QAction("Set as reference", self)
        self.set_themed_icon(self.set_ref_item, "icons/copy_link.svg")
        self.select_cntx_menu.addAction(self.set_ref_item)
        self.set_ref_item.triggered.connect(self._set_ref_item)

    def on_pkg_context_menu_requested(self, position):
        self.select_cntx_menu.exec(self._ui.pkgs_list_widget.mapToGlobal(position))

    @override
    def showEvent(self, event: QShowEvent) -> None:
        self._populate_left_items()
        self.update_diff()
        return super().showEvent(event)

    # public methods

    def add_diff_item(self, content: ConanPkg):
        """ " """
        self._item_data.append(content)

    def update_diff(self):
        """Resets the syntax highlighting, adds the different category items
        to the SyntaxHighlighter and then redraws them"""
        try:
            # reset diffs
            self._left_highlighter._reset_diff()
            self._right_highlighter._reset_diff()

            # set left and right content with first two diff elements
            pkg_diffs = list(diff(self._left_content, self._right_content))
            # filter other id
            pkg_diffs = list(filter(lambda diff: diff[1] != "id", pkg_diffs))
            for pkg_diff in pkg_diffs:
                diff_mode = pkg_diff[0]
                if diff_mode == "change":
                    if isinstance(pkg_diff[1], list):
                        key = str(pkg_diff[-1][1].split(".")[-1])
                    else:
                        key = str(pkg_diff[1].split(".")[-1])
                    value_left = str(pkg_diff[2][0])
                    value_right = str(pkg_diff[2][1])
                elif diff_mode == "add":
                    for detail_diff in pkg_diff[2]:
                        key = str(detail_diff[0])
                        value_right = str(detail_diff[1])
                        value_left = "INVALID"
                        self._left_highlighter.added_diffs.append(f"({key}: {value_left})")
                        self._right_highlighter.added_diffs.append(f"({key}: {value_right})")
                elif diff_mode == "remove":
                    for detail_diff in pkg_diff[2]:
                        key = str(detail_diff[0])
                        value_left = str(detail_diff[1])
                        value_right = "INVALID"
                        self._left_highlighter.removed_diffs.append(f"({key}: {value_left})")
                        self._right_highlighter.removed_diffs.append(f"({key}: {value_right})")
                self._left_highlighter.modified_diffs.append(f"({key}: {value_left})")
                self._right_highlighter.modified_diffs.append(f"({key}: {value_right})")
            self._left_highlighter.rehighlight()
            self._right_highlighter.rehighlight()
        except Exception as e:
            Logger().error("Diffing crashed: " + str(e))

    # internals

    def _on_item_changed(self, item: QListWidgetItem):
        """Set right content, when selection changes"""
        if not item:
            return
        sel_item_id = item.data(0)
        if "*" in sel_item_id:
            self._set_right_content(self._left_content)
            self.update_diff()
            return
        for item_data in self._item_data:
            if sel_item_id == item_data.get("id", ""):
                content = item_data
                self._set_right_content(content)
                self.update_diff()
                return

    def _filter_display_content(self, content: ConanPkg):
        """Only allow the following keys in the view"""
        new_content = {}
        new_content["settings"] = content.get("settings")
        new_content["options"] = content.get("options")
        new_content["requires"] = content.get("requires")
        return new_content

    def _set_left_content(self, content):
        self._left_content = self._filter_display_content(content)
        pkg_info = pformat(self._left_content).translate(
            {ord("{"): None, ord("}"): None, ord("'"): None}
        )
        self._ui.left_text_browser.setText(pkg_info)

    def _set_right_content(self, content):
        self._right_content = self._filter_display_content(content)
        pkg_info = pformat(self._right_content).translate(
            {ord("{"): None, ord("}"): None, ord("'"): None}
        )
        self._ui.right_text_browser.setText(pkg_info)

    def _set_ref_item(self):
        """Change the reference (*, left content)"""
        items = self._ui.pkgs_list_widget.selectedItems()
        if len(items) != 1:
            return
        sel_item = items[0]
        pkg_id = sel_item.data(0)
        for item in self._item_data:
            if item.get("id", "") == pkg_id:
                idx = self._item_data.index(item)
                self._item_data.pop(idx)
                self._item_data.insert(0, item)
                self._set_left_content(item)
                break
        self._ui.pkgs_list_widget.clear()
        self._populate_left_items()
        self.update_diff()

    def _populate_left_items(self):
        ref = self._set_diff_list_prios()

        self._set_left_content(ref)
        QListWidgetItem("* " + ref.get("id", "Unknown"), self._ui.pkgs_list_widget)
        for prio in range(0, 5):
            for content in self._item_data:
                if prio == content["prio"]:
                    item_name = content.get("id", "Unknown")
                    QListWidgetItem(item_name, self._ui.pkgs_list_widget)

        self._set_right_content(self._item_data[1])
        # select 2nd item so we see a diff per default
        self._ui.pkgs_list_widget.setCurrentRow(1)

    def _set_diff_list_prios(self):
        """Prioritize the list elements depending on the similarity of the original package
        Supports the most common settings.
        No option support yet.
        """
        item_data_len = len(self._item_data)
        if item_data_len < 1:
            return
        ref = self._item_data[0]
        ref["prio"] = -1

        item_data_len = len(self._item_data)
        for i in range(1, item_data_len):
            content = self._item_data[i]
            pkg_diffs = list(diff(ref, content))
            pkg_arch_diffs = list(filter(lambda diff: diff[1] == "settings.arch", pkg_diffs))
            content["prio"] = 5
            if pkg_arch_diffs:
                content["prio"] = 4
                continue
            pkg_os_diffs = list(filter(lambda diff: diff[1] == "settings.os", pkg_diffs))
            if pkg_os_diffs:
                content["prio"] = 3
                continue
            pkg_compiler_diffs = list(
                filter(lambda diff: diff[1] == "settings.compiler", pkg_diffs)
            )
            if pkg_compiler_diffs:
                content["prio"] = 2
                continue
            pkg_compiler_version_diffs = list(
                filter(lambda diff: diff[1] == ["settings", "compiler.version"], pkg_diffs)
            )
            if pkg_compiler_version_diffs:
                content["prio"] = 1
                continue
            if not pkg_compiler_version_diffs:
                content["prio"] = 0
        return ref
