#  Copyright 2026 European Union
#  Author: Bulgheroni Antonio (antonio.bulgheroni@ec.europa.eu)
#  SPDX-License-Identifier: EUPL-1.2
"""Dialog for manual editing of steering files in TOML format.

:Author: Bulgheroni Antonio (antonio.bulgheroni@ec.europa.eu)
:Description: Allow advanced users to edit the TOML serialization of the current steering builder.
"""

from __future__ import annotations

import re
import tempfile
from pathlib import Path
from typing import Any

from PySide6.QtCore import QTimer, Signal
from PySide6.QtGui import QColor, QFont, QSyntaxHighlighter, Qt, QTextCharFormat
from PySide6.QtWidgets import (
    QDialog,
    QHBoxLayout,
    QLabel,
    QMessageBox,
    QPlainTextEdit,
    QPushButton,
    QVBoxLayout,
    QWidget,
)

from mafw.steering.builder import SteeringBuilder
from mafw.steering_gui.controllers.steering_controller import SteeringController

#: Debounce duration for TOML validation in milliseconds.
_VALIDATION_DEBOUNCE_MS = 500


class TomlSyntaxHighlighter(QSyntaxHighlighter):
    """Simple TOML syntax highlighter for the manual editor."""

    def __init__(self, parent: Any = None) -> None:
        super().__init__(parent)
        self._comment_format = QTextCharFormat()
        self._comment_format.setForeground(QColor('#6a737d'))
        self._comment_format.setFontItalic(True)

        self._table_format = QTextCharFormat()
        self._table_format.setForeground(QColor('#005cc5'))
        self._table_format.setFontWeight(QFont.Weight.Bold)

        self._key_format = QTextCharFormat()
        self._key_format.setForeground(QColor('#22863a'))

        self._string_format = QTextCharFormat()
        self._string_format.setForeground(QColor('#b31d28'))

        self._number_format = QTextCharFormat()
        self._number_format.setForeground(QColor('#005cc5'))

        self._bool_format = QTextCharFormat()
        self._bool_format.setForeground(QColor('#6f42c1'))

        self._comment_pattern = re.compile(r'#.*$')
        self._table_pattern = re.compile(r'^\s*\[.*\]\s*$')
        self._key_pattern = re.compile(r'(?P<key>[A-Za-z0-9_.-]+)(?=\s*=)')
        self._string_pattern = re.compile(r'("([^"\\]|\\.)*"|\'(?:[^\'\\]|\\.)*\')')
        self._number_pattern = re.compile(r'\b\d+(?:\.\d+)?\b')
        self._bool_pattern = re.compile(r'\b(true|false)\b', re.IGNORECASE)

    def highlightBlock(self, text: str) -> None:  # noqa: N802
        table_match = self._table_pattern.match(text)
        if table_match:
            self.setFormat(0, len(text), self._table_format)

        for match in self._key_pattern.finditer(text):
            self.setFormat(match.start('key'), len(match.group('key')), self._key_format)

        for match in self._string_pattern.finditer(text):
            self.setFormat(match.start(0), len(match.group(0)), self._string_format)

        for match in self._number_pattern.finditer(text):
            self.setFormat(match.start(0), len(match.group(0)), self._number_format)

        for match in self._bool_pattern.finditer(text):
            self.setFormat(match.start(0), len(match.group(0)), self._bool_format)

        comment_match = self._comment_pattern.search(text)
        if comment_match:
            self.setFormat(comment_match.start(0), len(comment_match.group(0)), self._comment_format)


class SteeringTextEditor(QDialog):
    """Dialog that lets users edit the steering file directly as TOML."""

    builder_applied = Signal()

    def __init__(self, controller: SteeringController, parent: QWidget | None = None) -> None:
        super().__init__(parent)
        self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose)
        self._controller = controller
        self.setMinimumSize(600, 500)
        self._temp_path: Path | None = None
        self._skip_dirty_prompt = False
        self._is_valid = False
        self._validation_message: str | None = None

        filename = self._current_filename()
        self._base_title = f'Steering file: {filename}'
        self.setWindowTitle(self._base_title)

        layout = QVBoxLayout(self)
        layout.addWidget(QLabel(f'Steering file: {filename}'))
        layout.addWidget(QLabel('Note: formatting and comments may be lost after saving'))

        self._editor = QPlainTextEdit(self)
        self._editor.setLineWrapMode(QPlainTextEdit.LineWrapMode.NoWrap)
        TomlSyntaxHighlighter(self._editor.document())
        layout.addWidget(self._editor, 1)

        self._file_status_label = QLabel()
        layout.addWidget(self._file_status_label)

        buttons_row = QHBoxLayout()
        buttons_row.addStretch(1)
        self._cancel_button = QPushButton('Cancel')
        self._save_button = QPushButton('Save')
        self._apply_button = QPushButton('Apply and close')
        buttons_row.addWidget(self._cancel_button)
        buttons_row.addWidget(self._save_button)
        buttons_row.addWidget(self._apply_button)
        layout.addLayout(buttons_row)

        self._validation_timer = QTimer(self)
        self._validation_timer.setSingleShot(True)
        self._validation_timer.timeout.connect(self._validate_current_text)

        self._editor.document().modificationChanged.connect(self._on_modified_changed)
        self._editor.textChanged.connect(self._schedule_validation)
        self._cancel_button.clicked.connect(self._request_cancel)
        self._save_button.clicked.connect(self._save_temp_file)
        self._apply_button.clicked.connect(self._apply_and_close)

        if not self._load_initial_text():
            return
        self._schedule_validation()

    def closeEvent(self, event: Any) -> None:  # noqa: N802
        if not self._skip_dirty_prompt and self._editor.document().isModified():
            if not self._confirm_discard():
                event.ignore()
                return
        self._cleanup_temp_file()
        super().closeEvent(event)

    def _current_filename(self) -> str:
        current = self._controller.current_file()
        return current.name if current is not None else 'Untitled'

    def _load_initial_text(self) -> bool:
        try:
            temp_handle = tempfile.NamedTemporaryFile(delete=False, suffix='.toml')
            temp_handle.close()
            self._temp_path = Path(temp_handle.name)
            self._controller.export_to_path(self._temp_path)
            text = self._temp_path.read_text(encoding='utf-8')
        except Exception as exc:  # noqa: BLE001
            QMessageBox.critical(self, 'Failed to open editor', str(exc))
            self._cleanup_temp_file()
            self.reject()
            return False

        self._editor.setPlainText(text)
        self._editor.document().setModified(False)
        self._update_status_label()
        return True

    def _schedule_validation(self) -> None:
        self._validation_timer.start(_VALIDATION_DEBOUNCE_MS)

    def _on_modified_changed(self, modified: bool) -> None:
        self._update_window_title(modified)
        self._update_status_label()

    def _update_window_title(self, modified: bool) -> None:
        title = self._base_title
        if modified:
            title += ' *'
        self.setWindowTitle(title)

    def _update_status_label(self) -> None:
        modified = self._editor.document().isModified()
        state = 'Modified' if modified else 'Saved'
        validity = 'valid' if self._is_valid else 'invalid'
        self._file_status_label.setText(f'{state} and {validity}')
        self._file_status_label.setToolTip(self._validation_message or '')
        self._apply_button.setEnabled(self._is_valid)

    def _validate_current_text(self) -> None:
        text = self._editor.toPlainText()
        validation_level = self._controller.validation_level()
        try:
            builder = SteeringBuilder.from_toml_text(text)
            if validation_level is None:
                self._set_validation_state(True, None)
                return
            issues = builder.validate(validation_level)
        except Exception as exc:  # noqa: BLE001
            self._set_validation_state(False, str(exc))
            return

        if issues:
            self._set_validation_state(False, str(issues[0]))
        else:
            self._set_validation_state(True, None)

    def _set_validation_state(self, valid: bool, message: str | None) -> None:
        self._is_valid = valid
        self._validation_message = message
        self._update_status_label()

    def _request_cancel(self) -> None:
        self.close()

    def _confirm_discard(self) -> bool:
        prompt = QMessageBox(self)
        prompt.setWindowTitle('Discard changes?')
        prompt.setText('Discard your edits to the steering file?')
        prompt.setStandardButtons(
            QMessageBox.StandardButton.Discard | QMessageBox.StandardButton.Cancel,
        )
        prompt.setDefaultButton(QMessageBox.StandardButton.Cancel)
        response = prompt.exec()
        return response == QMessageBox.StandardButton.Discard

    def _save_temp_file(self) -> None:
        self._validate_current_text()
        if self._temp_path is None:
            return
        try:
            self._temp_path.write_text(self._editor.toPlainText(), encoding='utf-8')
        except Exception as exc:  # noqa: BLE001
            QMessageBox.critical(self, 'Failed to save', str(exc))
            return

        if not self._is_valid:
            QMessageBox.warning(self, 'Validation issues', self._validation_message or 'Invalid steering file.')
            self._update_status_label()
            return

        self._editor.document().setModified(False)
        self._update_status_label()

    def _apply_and_close(self) -> None:
        self._validate_current_text()
        if not self._is_valid:
            QMessageBox.warning(self, 'Validation issues', self._validation_message or 'Invalid steering file.')
            return
        if self._temp_path is None:
            return
        try:
            self._temp_path.write_text(self._editor.toPlainText(), encoding='utf-8')
            builder = SteeringBuilder.from_toml(self._temp_path)
        except Exception as exc:  # noqa: BLE001
            QMessageBox.critical(self, 'Failed to apply', str(exc))
            return

        self._controller.replace_builder(builder)
        self.builder_applied.emit()
        self._skip_dirty_prompt = True
        self.accept()

    def _cleanup_temp_file(self) -> None:
        if self._temp_path is None:
            return
        try:
            self._temp_path.unlink(missing_ok=True)
        except Exception:  # noqa: BLE001
            pass
        self._temp_path = None
