from loguru import logger
from logging import getLogger, getLevelName
from common.gui.forms.settings_window import Ui_SettingsWindow
from common.lib.enums.TextConstants import TextConstants
from common.gui.enums.KeySequences import KeySequences
from common.lib.constants import LogDefinition
from common.lib.data_models.Config import Config
from common.lib.enums.TermFilesPath import TermFilesPath
from common.gui.decorators.window_settings import set_window_icon, has_close_button_only
from common.gui.enums.GuiFilesPath import GuiFilesPath
from common.lib.enums.ReleaseDefinition import ReleaseDefinition
from PyQt6.QtMultimedia import QAudioOutput, QMediaPlayer
from PyQt6.QtCore import Qt, QUrl, QRegularExpression, pyqtSignal
from PyQt6.QtWidgets import QDialog, QDialogButtonBox, QApplication
from PyQt6.QtGui import (
    QKeySequence,
    QShortcut,
    QRegularExpressionValidator,
    QIntValidator,
    QPixmap,
    QIcon,
    QDesktopServices,
    QCloseEvent,
    QKeyEvent,
    QMovie,
)


class SettingsWindow(Ui_SettingsWindow, QDialog):
    open_user_guide: pyqtSignal = pyqtSignal(str)
    _config: Config = None
    config_file_dropped: pyqtSignal = pyqtSignal(str)
    open_api_spec: pyqtSignal = pyqtSignal()
    movie: QMovie

    @property
    def config(self):
        return self._config

    @config.setter
    def config(self, config: Config):
        self._config = config

    def __init__(self, config: Config, about: bool = False):
        super().__init__()
        self.setupUi(self)
        self.config = config
        self.audio_output = QAudioOutput()
        self.player = QMediaPlayer()
        self.setup(about=about)

    @set_window_icon
    @has_close_button_only
    def setup(self, about: bool = False) -> None:
        self.setAcceptDrops(True)
        self.player.setAudioOutput(self.audio_output)
        self.player.setSource(QUrl.fromLocalFile(GuiFilesPath.VVVVVV))
        self.MainTabs.tabBar().setDocumentMode(True)
        self.MainTabs.tabBar().setExpanding(True)
        self.SvAddress.setValidator(QRegularExpressionValidator(QRegularExpression(r"(\d+\.){1,3}\d+")))
        self.MaxAmount.setEditable(True)
        self.RemoteSpecUrl.editingFinished.connect(lambda: self.RemoteSpecUrl.setCursorPosition(int()))
        self.MaxAmount.setValidator(QIntValidator(1, 2_100_000_000, self.MaxAmount))
        self.DebugLevel.addItems(LogDefinition.LOG_LEVEL)
        self.UserGuideLink.setText(TextConstants.USER_REFERENCE_GUIDE)
        self.ApiInfoLabel.setText(TextConstants.API_EXPLANATION_URL)

        for button_box in self.GeneralButtonBox, self.FieldsButtonBox, self.ApiButtonBox, self.SpecificationButtonBox:
            button_box.accepted.connect(self.ok)
            button_box.rejected.connect(self.cancel)
            button_box.clicked.connect(self.process_default_button)

        self.HeaderLength.textChanged.connect(self.validate_header_length)
        self.KeepAliveMode.stateChanged.connect(lambda state: self.KeepAliveInterval.setEnabled(bool(state)))
        self.HeaderLengthMode.stateChanged.connect(lambda state: self.HeaderLength.setEnabled(bool(state)))
        self.MaxAmountBox.stateChanged.connect(lambda state: self.MaxAmount.setEnabled(bool(state)))
        self.LoadSpecGeneral.stateChanged.connect(lambda: self.LoadSpec.setChecked(self.LoadSpecGeneral.isChecked()))
        self.LoadSpec.stateChanged.connect(lambda: self.LoadSpecGeneral.setChecked(self.LoadSpec.isChecked()))
        self.ApiRun.stateChanged.connect(lambda: self.ApiRunGeneral.setCheckState(self.ApiRun.checkState()))
        self.ApiRunGeneral.stateChanged.connect(lambda: self.ApiRun.setCheckState(self.ApiRunGeneral.checkState()))

        self.LogBackupStorageExists.stateChanged.connect(
            lambda: self.LogStorageDepth.setEnabled(self.LogBackupStorageExists.isChecked())
        )

        self.SpecBackupStorageExists.stateChanged.connect(
            lambda: self.StorageDepth.setEnabled(self.SpecBackupStorageExists.isChecked())
        )

        self.ValidationEnabled.stateChanged.connect(self.process_validation_change)
        self.ApiInfoLabel.linkActivated.connect(self.open_api_spec)
        self.MusicOnOfButton.clicked.connect(self.switch_music)
        self.ContactLabel.linkActivated.connect(self.open_url)
        self.CopySpecUrl.clicked.connect(self.copy_remote_spec_url)
        self.process_validation_change()
        self.process_config(self.config)
        self.set_data_about()
        self.MainTabs.setCurrentIndex(self.MainTabs.count() - 1 if about else int())
        self.UserGuideLink.linkActivated.connect(self.open_user_guide)
        self.ManualInputMode.setChecked(False)
        self.ManualInputMode.hide()

        QShortcut(QKeySequence(KeySequences.CTRL_PAGE_DOWN), self).activated.connect(
            lambda: self.MainTabs.setCurrentIndex(self.MainTabs.currentIndex() + 1)
        )

        QShortcut(QKeySequence(KeySequences.CTRL_PAGE_UP), self).activated.connect(
            lambda: self.MainTabs.setCurrentIndex(self.MainTabs.currentIndex() - 1)
        )

    def process_config(self, config: Config) -> None:
        checkboxes_state_map = {
            self.MaxAmountBox: config.fields.max_amount_limited,
            self.ProcessDefaultDump: config.terminal.process_default_dump,
            self.ConnectOnStartup: config.terminal.connect_on_startup,
            self.ClearLog: config.debug.clear_log,
            self.BuildFld90: config.fields.build_fld_90,
            self.SendInternalId: config.fields.send_internal_id,
            self.ValidateWindow: config.validation.validate_window,
            self.JsonMode: config.fields.json_mode,
            self.KeepAliveMode: config.host.keep_alive_mode,
            self.HeaderLengthMode: config.host.header_length_exists,
            self.HideSecrets: config.fields.hide_secrets,
            self.RewriteLocalSpec: config.specification.rewrite_local_spec,
            self.LoadSpecGeneral: config.terminal.load_remote_spec,
            self.ShowLicense: config.terminal.show_license_dialog,
            self.ValidateIncoming: config.validation.validate_incoming,
            self.ValidateOutgoing: config.validation.validate_outgoing,
            self.ManualInputMode: config.specification.manual_input_mode,
            self.ValidationEnabled: config.validation.validation_enabled,
            self.ApiRun: config.terminal.run_api,
            self.ReduceKeepAlive: config.debug.reduce_keep_alive,
            self.WaitForRemoteHost: config.api.wait_remote_host_response,
            self.HideSecretsApi: config.api.hide_secrets,
            self.ParseComplexFields: config.api.parse_subfields,
            self.PrintSubfields: config.debug.parse_subfields,
            self.PrintDescription: config.debug.print_description,
            self.BackupLocalSpecStartup: config.specification.backup_on_startup,
            self.BackupLocalSpecShutdown: config.specification.backup_on_shutdown,
            self.LogBackupStorageExists: config.debug.backup_storage_depth_exists,
            self.SpecBackupStorageExists: config.specification.backup_storage,
        }

        scales_value_map = {
            self.SvPort: config.host.port,
            self.KeepAliveInterval: config.host.keep_alive_interval,
            self.HeaderLength: config.host.header_length,
            self.StorageDepth: config.specification.backup_storage_depth,
            self.LogStorageDepth: config.debug.backup_storage_depth,
            self.ApiPort: self.config.api.port,
            self.ApiTimeout: self.config.api.waiting_timeout_seconds,
        }

        for checkbox, state in checkboxes_state_map.items():
            checkbox.setChecked(state)

        enabled_dependence = {
            self.HeaderLength: lambda: config.host.header_length_exists,
            self.KeepAliveInterval: self.KeepAliveMode.isChecked,
            self.MaxAmount: self.MaxAmountBox.isChecked,
            self.StorageDepth: self.SpecBackupStorageExists.isChecked,
            self.LogStorageDepth: self.LogBackupStorageExists.isChecked,
        }

        for item, enabled in enabled_dependence.items():
            item.setEnabled(enabled())

        for scale, value in scales_value_map.items():
            scale.setValue(int(value))

        self.DebugLevel.setCurrentText(config.debug.level)
        self.SvAddress.setText(config.host.host)
        self.RemoteSpecUrl.setText(config.specification.remote_spec_url)
        self.RemoteSpecUrl.setCursorPosition(int())
        self.ValidationReaction.setCurrentIndex(self.ValidationReaction.findText(config.validation.validation_mode))

        if not config.fields.max_amount_limited:
            return

        max_amount: str = str(config.fields.max_amount)

        if (index := self.MaxAmount.findText(max_amount)) < int():  # If the max_amount from config is not found
            index: int = int()
            self.MaxAmount.insertItem(index, max_amount)

        self.MaxAmount.setCurrentIndex(index)

    def dragEnterEvent(self, event):
        if not event.mimeData().hasUrls():
            event.ignore()
            return

        event.acceptProposedAction()

    def dragMoveEvent(self, event):
        if not event.mimeData().hasUrls():
            event.ignore()
            return

        event.acceptProposedAction()

    def dropEvent(self, event):
        if not event.mimeData().urls():
            event.ignore()
            return

        for url in event.mimeData().urls():

            if not (config_file := url.toLocalFile()):
                continue

            logger.debug(f"Config file dropped {config_file}")

            try:
                with open(config_file) as json_file:
                    config: Config = Config.model_validate_json(json_file.read())

            except Exception as config_parsing_error:
                logger.error(f"Config parsing_error: {config_parsing_error}")
                continue

            try:
                self.process_config(config)

            except Exception as config_processing_error:
                logger.error(config_processing_error)
                continue

            logger.info(f"Config file parsed: {config_file}")

        event.acceptProposedAction()

    def copy_remote_spec_url(self):
        QApplication.clipboard().setText(self.RemoteSpecUrl.text())

    def process_default_button(self, button):
        for button_box in self.GeneralButtonBox, self.FieldsButtonBox, self.ApiButtonBox, self.SpecificationButtonBox:
            if button_box.buttonRole(button) == QDialogButtonBox.ButtonRole.ResetRole:
                self.set_default_settings()
                break

    def process_validation_change(self) -> None:
        validation_elements = (
            self.ValidateWindow,
            self.ValidateIncoming,
            self.ValidateOutgoing,
            self.ValidationModeLabel,
            self.ValidationReaction,
        )

        for element in validation_elements:
            element.setEnabled(self.ValidationEnabled.isChecked())

    def set_data_about(self) -> None:
        self.logoLabel.setPixmap(QPixmap(GuiFilesPath.SIGNED_LOGO))
        self.MusicOnOfButton.setIcon(QIcon(QPixmap(GuiFilesPath.MUSIC_ON)))

        data_bind = {
            self.VersionLabel: ReleaseDefinition.VERSION,
            self.ReleaseLabel: ReleaseDefinition.RELEASE,
            self.ContactLabel: ReleaseDefinition.CONTACT,
            self.AuthorLabel: ReleaseDefinition.AUTHOR,
        }

        for label, text in data_bind.items():
            label.setText(f"{label.text()} {text}")

        self.player.playbackStateChanged.connect(self.record_finished)

    def set_default_settings(self) -> None:
        try:
            with open(TermFilesPath.DEFAULT_CONFIG) as json_file:
                default_config: Config = Config.model_validate_json(json_file.read())

        except Exception as parsing_error:
            logger.error(parsing_error)
            return

        default_config.host.host = self.config.host.host
        default_config.host.port = self.config.host.port
        default_config.specification.remote_spec_url = self.config.specification.remote_spec_url
        default_config.specification.rewrite_local_spec = self.config.specification.rewrite_local_spec
        default_config.api.address = self.config.api.address
        default_config.api.port = self.config.api.port

        self.process_config(default_config)

    def validate_header_length(self) -> None:
        header_length: int = int(self.HeaderLength.value())

        if self.HeaderLengthMode.isChecked() and self.HeaderLength.value() < 2:
            self.HeaderLength.setValue(2)

        if header_length % 2 != int():
            self.HeaderLength.setValue(header_length - 1)

    def ok(self) -> None:
        getLogger().setLevel(getLevelName(self.DebugLevel.currentText()))

        config = self.config

        config.host.host = self.SvAddress.text()
        config.host.port = self.SvPort.value()
        config.host.keep_alive_mode = self.KeepAliveMode.isChecked()
        config.host.header_length_exists = self.HeaderLengthMode.isChecked()
        config.host.keep_alive_interval = self.KeepAliveInterval.value()
        config.host.header_length = self.HeaderLength.value()
        config.terminal.process_default_dump = self.ProcessDefaultDump.isChecked()
        config.terminal.connect_on_startup = self.ConnectOnStartup.isChecked()
        config.terminal.load_remote_spec = self.LoadSpec.isChecked()
        config.terminal.show_license_dialog = self.ShowLicense.isChecked()
        config.debug.backup_storage_depth = self.LogStorageDepth.value()
        config.debug.clear_log = self.ClearLog.isChecked()
        config.debug.level = self.DebugLevel.currentText()
        config.fields.max_amount_limited = self.MaxAmountBox.isChecked()
        config.fields.max_amount = int(self.MaxAmount.currentText())
        config.fields.build_fld_90 = self.BuildFld90.isChecked()
        config.fields.send_internal_id = self.SendInternalId.isChecked()
        config.fields.json_mode = self.JsonMode.isChecked()
        config.fields.hide_secrets = self.HideSecrets.isChecked()
        config.specification.backup_storage_depth = self.StorageDepth.value()
        config.validation.validate_window = self.ValidateWindow.isChecked()
        config.validation.validate_incoming = self.ValidateIncoming.isChecked()
        config.validation.validate_outgoing = self.ValidateOutgoing.isChecked()
        config.validation.validation_mode = self.ValidationReaction.currentText()
        config.specification.remote_spec_url = self.RemoteSpecUrl.text()
        config.specification.manual_input_mode = self.ManualInputMode.isChecked()
        config.validation.validation_enabled = self.ValidationEnabled.isChecked()
        config.debug.reduce_keep_alive = self.ReduceKeepAlive.isChecked()
        config.api.port = self.ApiPort.value()
        config.api.wait_remote_host_response = self.WaitForRemoteHost.isChecked()
        config.api.hide_secrets = self.HideSecretsApi.isChecked()
        config.api.waiting_timeout_seconds = self.ApiTimeout.value()
        config.terminal.run_api = self.ApiRun.isChecked()
        config.api.parse_subfields = self.ParseComplexFields.isChecked()
        config.debug.parse_subfields = self.PrintSubfields.isChecked()
        config.debug.print_description = self.PrintDescription.isChecked()
        config.specification.backup_on_shutdown = self.BackupLocalSpecShutdown.isChecked()
        config.specification.backup_on_startup = self.BackupLocalSpecStartup.isChecked()
        config.debug.backup_storage_depth_exists = self.LogBackupStorageExists.isChecked()
        config.specification.backup_storage = self.SpecBackupStorageExists.isChecked()
        config.specification.rewrite_local_spec = all(
            [
                self.RewriteLocalSpec.isChecked(),
                self.RewriteLocalSpec.isEnabled(),
            ]
        )

        if not config.fields.max_amount_limited:
            config.fields.max_amount = 9_999_999_999

        try:
            with open(TermFilesPath.CONFIG, "w") as file:
                file.write(self.config.model_dump_json(indent=4))

        except Exception as config_write_error:
            logger.error(f"Cannon update config: {config_write_error}")
            self.cancel()
            return

        self.accept()

    def cancel(self) -> None:
        logger.info("Settings applying was canceled")
        self.reject()

    @staticmethod
    def open_url(link, ) -> None:
        link = QUrl(link)
        QDesktopServices.openUrl(link)

    def record_finished(self, state) -> None:
        if state == self.player.PlaybackState.StoppedState:
            self.MusicOnOfButton.setIcon(QIcon(QPixmap(GuiFilesPath.MUSIC_ON)))

    def switch_music(self) -> None:
        match self.player.playbackState():
            case self.player.PlaybackState.StoppedState:
                icon = GuiFilesPath.MUSIC_OFF
                self.player.play()

            case self.player.PlaybackState.PlayingState:
                icon = GuiFilesPath.MUSIC_ON
                self.player.stop()

            case _:
                return

        self.MusicOnOfButton.setIcon(QIcon(QPixmap(icon)))

    def closeEvent(self, a0: QCloseEvent) -> None:
        self.player.stop()
        a0.accept()

    def keyPressEvent(self, a0: QKeyEvent) -> None:
        if a0.key() == Qt.Key.Key_Escape:
            self.close()

        if a0.key() in (Qt.Key.Key_Enter, Qt.Key.Key_Return):
            if self.MainTabs.currentIndex() == self.MainTabs.count() - 1:
                return

            self.GeneralButtonBox.accepted.emit()
