import datetime
import logging
from collections import namedtuple
from typing import List

import yaml
from PyQt5.QtWidgets import QVBoxLayout
from qgis.PyQt import QtCore, QtWidgets, QtGui
from qgis.PyQt.QtCore import Qt, QSettings
from qgis.PyQt.QtWidgets import QFrame, QSpacerItem, QSizePolicy, QGridLayout, QGroupBox

from sqlalchemy import null
from sqlalchemy.orm import class_mapper, MapperProperty, RelationshipProperty
from sqlalchemy.orm.exc import UnmappedClassError

from SAGisXPlanung import Session
from SAGisXPlanung.XPlan.types import InvalidFormException
from SAGisXPlanung.config import xplan_tooltip, export_version
from SAGisXPlanung.gui.widgets.inputs.QRelationDropdowns import QAddRelationDropdown
from SAGisXPlanung.gui.widgets.inputs.input_widgets import QFileInput, QMultiInputWidget
from SAGisXPlanung.gui.widgets.inputs.base_input_element import BaseInputElement

PYQT_DEFAULT_DATE = datetime.date(1752, 9, 14)
logger = logging.getLogger(__name__)


class DataInputPage(QtWidgets.QScrollArea):

    addRelationRequested = QtCore.pyqtSignal(object, bool, str)
    parentLinkClicked = QtCore.pyqtSignal()
    requestPageToTop = QtCore.pyqtSignal()

    def __init__(self, cls_type, parent_class, parent_attribute=None, existing_xid=None, *args, **kwargs):
        super(DataInputPage, self).__init__(*args, **kwargs)
        self.cls_type = cls_type
        self.parent_class = parent_class
        self.parent_attribute = parent_attribute
        self.existing_xid = existing_xid
        self.child_pages = []
        self.fields = {}
        self.required_inputs = []

        s = QSettings()
        self.ATTRIBUTE_CONFIG = yaml.safe_load(s.value(f"plugins/xplanung/attribute_config", '')) or {}

        self.setFrameShape(QFrame.NoFrame)
        self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
        self.horizontalScrollBar().setEnabled(False)
        self.setSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum)

        self.content_widget = QtWidgets.QWidget()
        self.setWidget(self.content_widget)
        self.vBox = QtWidgets.QVBoxLayout()
        self.vBox.setSpacing(20)
        self.content_widget.setLayout(self.vBox)

        if self.parent_class is not None:
            self.createHeader()
        self.createLayout()

        spacer = QtWidgets.QWidget()
        spacer.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Expanding)
        self.vBox.addWidget(spacer)

    def sizeHint(self):
        size = super(DataInputPage, self).sizeHint()
        size.setWidth(size.width() + 5 * self.verticalScrollBar().sizeHint().width())
        return size

    def addChildPage(self, page):
        self.child_pages.append(page)

    def removeChildPage(self, page):
        self.child_pages.remove(page)

    def createHeader(self):
        widget = QtWidgets.QWidget(self)
        widget.setStyleSheet("""
            QLabel#lParentAttribute {
                color: #64748b;
            }
            QPushButton#lParentClass{
                color: #1e293b;
                background: palette(window);
                border: 0px;
            }
            QPushButton#lParentClass:hover {
                color: #0ea5e9;
            }
        """)
        h_layout = QtWidgets.QHBoxLayout(widget)
        h_layout.setContentsMargins(0, 10, 0, 10)
        l1 = QtWidgets.QLabel('Objekt gehört zu:')
        l2 = QtWidgets.QPushButton(self.parent_class.__name__, objectName='lParentClass')
        l1.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred)
        l2.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred)
        h_layout.addWidget(l1)
        h_layout.addWidget(l2)
        if self.parent_attribute:
            label_display_name = self.parent_class.xplan_attribute_name(self.parent_attribute)

            l3 = QtWidgets.QLabel(f'(Attribut: {label_display_name})', objectName='lParentAttribute')
            l2.setCursor(Qt.PointingHandCursor)
            l2.clicked.connect(lambda checked: self.parentLinkClicked.emit())
            h_layout.addWidget(l3)
        else:
            l2.setDisabled(True)
            h_layout.addItem(QSpacerItem(20, 10, QSizePolicy.Expanding, QSizePolicy.Minimum))
        self.vBox.addWidget(widget)

    def createLayout(self):
        column_resizer = ColumnResizer()

        for cls in self.cls_type.mro():
            # catches base classes which are no sqlalchemy models
            if not is_mapped(cls):
                continue

            group_box = QGroupBox(cls.__name__)
            grid = QtWidgets.QGridLayout(group_box)

            col_skip = 0
            for i, (key, prop) in enumerate(cls.element_order(include_base=False, export=False,
                                                              ret_fmt='sqla', version=export_version())):
                # don't show attributes that are disabled in settings
                if key in self.ATTRIBUTE_CONFIG.get(cls.__name__, []):
                    col_skip += 1
                    continue

                label, control = self.create_input(key, prop)
                control.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)

                # wrap label in spacer items, to keep it aligned with first row of control widget
                # when the control widget has expanding rows
                if isinstance(control, QMultiInputWidget):
                    spacer_container = QVBoxLayout()
                    spacer_container.addItem(QSpacerItem(20, 5, QSizePolicy.Minimum, QSizePolicy.Minimum))
                    spacer_container.addWidget(label)
                    spacer_container.addItem(QSpacerItem(20, 0, QSizePolicy.Minimum, QSizePolicy.Expanding))

                    grid.addLayout(spacer_container, i - col_skip, 0)
                else:
                    grid.addWidget(label, i - col_skip, 0)

                grid.addWidget(control, i - col_skip, 1)

                self.fields[key] = control

            # don't add group boxes that are empty
            if grid.count() == 0:
                continue

            column_resizer.form_widgets.append(grid)
            self.vBox.addWidget(group_box)

        column_resizer.applyResize()

    def onRelationButtonClicked(self, class_type, uselist, parent_attribute):
        if not uselist:
            button = self.fields[parent_attribute]
            button.setDisabled(True)
        self.addRelationRequested.emit(class_type, uselist, parent_attribute)

    def create_input(self, attr_name: str, mapper_property: MapperProperty):
        cls = mapper_property.parent.class_

        if isinstance(mapper_property, RelationshipProperty):
            form_type = mapper_property.info.get('form-type')
            label_display_name, tooltip = cls.relation_prop_display((attr_name, mapper_property))
            label = QtWidgets.QLabel(label_display_name)
            label.setObjectName(attr_name)
            if tooltip:
                label.setToolTip(tooltip)
            if mapper_property.info.get('nullable') is False:
                label.setStyleSheet("font-weight: bold")

            if form_type == 'inline' or mapper_property.secondary:
                stub = namedtuple('stub', ['cls_type'])
                control = QAddRelationDropdown(stub(cls_type=self.cls_type), (attr_name, mapper_property))

                return label, control

            # one-to-many relation, one-to-one relation (defined by `uselist=False`)
            else:
                widget = QtWidgets.QPushButton("Hinzufügen")
                widget.clicked.connect(lambda state, c=mapper_property.mapper.class_,
                                              u=mapper_property.uselist, a=attr_name:
                                       self.onRelationButtonClicked(c, u, a))
                return label, widget

        # base_classes = [c for c in list(getmro(cls)) if issubclass(c, Base)]
        # cls = next(c for c in base_classes if
        #            hasattr(c, label_name) and c.attr_fits_version(label_name, export_version()))
        column = getattr(cls, attr_name).property.columns[0]
        field_type = column.type
        nullable = column.nullable

        control = BaseInputElement.create(field_type, self)
        xplan_name = cls.xplan_attribute_name(attr_name)

        if column.doc:
            label = QtWidgets.QLabel(column.doc)
            label.setToolTip(f'XPlanung-Attribut: {xplan_name}')
        else:
            label = QtWidgets.QLabel(xplan_name)
            tooltip = xplan_tooltip(cls, xplan_name)
            label.setToolTip(tooltip)

        label.setObjectName(attr_name)

        if not nullable:
            font = QtGui.QFont()
            font.setBold(True)
            label.setFont(font)
            self.required_inputs.append(attr_name)

        return label, control

    def getObjectFromInputs(self, validate_forms=True):
        if validate_forms and not self.validateForms():
            raise InvalidFormException()

        obj = self.cls_type()
        if self.existing_xid:
            obj.id = self.existing_xid

        for column, input_field in self.fields.items():
            if isinstance(input_field, QAddRelationDropdown):
                class_type = getattr(obj.__class__, column).property.mapper.class_
                if input_field.relation[1].secondary is None:
                    selected_object = input_field.value()
                    if not selected_object:  # no relation selected
                        setattr(obj, f'{column}_id', null())
                        continue
                    with Session.begin() as session:
                        obj_from_db = session.merge(selected_object)
                        setattr(obj, f'{column}_id', obj_from_db.id)
                    setattr(obj, column, obj_from_db)
                else:
                    selected_objects = input_field.value()
                    with Session.begin() as session:
                        obj_list = []
                        for selected_item in input_field.value():
                            obj_list.append(session.merge(selected_item))
                    setattr(obj, column, obj_list)
                continue

            if not isinstance(input_field, BaseInputElement):
                continue

            setattr(obj, column, input_field.value())

            if isinstance(input_field, QFileInput):
                setattr(obj, 'file', input_field.file())

        return obj

    def validateForms(self):
        is_valid = True
        invalid_attributes = []
        for attribute, control in self.fields.items():
            if not isinstance(control, BaseInputElement):
                continue

            required_input = bool(attribute in self.required_inputs)
            if not control.validate_widget(required_input):
                is_valid = False
                control.setInvalid(True)
                invalid_attributes.append(attribute)

        if not is_valid:
            label = self.findChild(QtWidgets.QLabel, invalid_attributes[0])
            self.ensureWidgetVisible(label)
            self.requestPageToTop.emit()

        return is_valid


def is_mapped(obj):
    try:
        class_mapper(obj)
    except UnmappedClassError:
        return False
    return True

# ****************************************************************


class ColumnResizer:
    """
    Helper-Klasse zum Anpassen des Layouts der Eingabeformulare.
    Gleicht die Breite der Spalten mehrerer voneinander unabhängiger GridLayouts an.

    Inspiriert durch https://github.com/agateau/columnresizer
    """

    def __init__(self):
        self.form_widgets: List[QGridLayout] = []

    def applyResize(self):
        largest_size = 0

        # find largest label size
        for widget in self.form_widgets:
            # prevent empty layouts
            if widget.rowCount() < 1:
                continue
            for i in range(widget.rowCount()):
                label = widget.itemAtPosition(i, 0).widget()
                if not label:
                    continue
                size = label.minimumSizeHint().width()
                largest_size = max(largest_size, size)

        # apply resizing: set all labels to minimum width
        for widget in self.form_widgets:
            if widget.rowCount() < 1:
                continue
            for i in range(widget.rowCount()):
                label = widget.itemAtPosition(i, 0).widget()
                if not label:
                    continue
                label.setMinimumWidth(largest_size)
