"""
/***************************************************************************
Name                 : Versioning plugin for DB Manager
Description          : Set up versioning support for a table
Date                 : Mar 12, 2012
copyright            : (C) 2012 by Giuseppe Sucameli
email                : brush.tyler@gmail.com

Based on PG_Manager by Martin Dobias <wonder.sk@gmail.com> (GPLv2 license)
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/
"""

from pathlib import Path

from qgis.PyQt import uic
from qgis.PyQt.QtCore import Qt
from qgis.PyQt.QtWidgets import QApplication, QDialog, QDialogButtonBox, QMessageBox

from .....dlg_db_error import DlgDbError
from ....plugin import BaseError, Table

Ui_DlgVersioning, _ = uic.loadUiType(Path(__file__).parent / "DlgVersioning.ui")


class DlgVersioning(QDialog, Ui_DlgVersioning):
    def __init__(self, item, parent=None):
        QDialog.__init__(self, parent)
        self.item = item
        self.setupUi(self)

        self.db = self.item.database()
        self.schemas = self.db.schemas()
        self.hasSchemas = self.schemas is not None

        self.buttonBox.accepted.connect(self.onOK)
        self.buttonBox.helpRequested.connect(self.showHelp)

        self.populateSchemas()
        self.populateTables()

        if isinstance(item, Table):
            index = self.cboTable.findText(self.item.name)
            if index >= 0:
                self.cboTable.setCurrentIndex(index)

        self.cboSchema.currentIndexChanged.connect(self.populateTables)

        # updates of SQL window
        self.cboSchema.currentIndexChanged.connect(self.updateSql)
        self.cboTable.currentIndexChanged.connect(self.updateSql)
        self.chkCreateCurrent.stateChanged.connect(self.updateSql)
        self.editPkey.textChanged.connect(self.updateSql)
        self.editStart.textChanged.connect(self.updateSql)
        self.editEnd.textChanged.connect(self.updateSql)
        self.editUser.textChanged.connect(self.updateSql)

        self.updateSql()

    def populateSchemas(self):
        self.cboSchema.clear()
        if not self.hasSchemas:
            self.hideSchemas()
            return

        index = -1
        for schema in self.schemas:
            self.cboSchema.addItem(schema.name)
            if hasattr(self.item, "schema") and schema.name == self.item.schema().name:
                index = self.cboSchema.count() - 1
        self.cboSchema.setCurrentIndex(index)

    def hideSchemas(self):
        self.cboSchema.setEnabled(False)

    def populateTables(self):
        self.tables = []

        schemas = self.db.schemas()
        if schemas is not None:
            schema_name = self.cboSchema.currentText()
            matching_schemas = [x for x in schemas if x.name == schema_name]
            tables = matching_schemas[0].tables() if len(matching_schemas) > 0 else []
        else:
            tables = self.db.tables()

        self.cboTable.clear()
        for table in tables:
            if table.type == table.VectorType:  # contains geometry column?
                self.tables.append(table)
                self.cboTable.addItem(table.name)

    def get_escaped_name(self, schema, table, suffix):
        name = self.db.connector.quoteId(f"{table}{suffix}")
        schema_name = self.db.connector.quoteId(schema) if schema else None
        return f"{schema_name}.{name}" if schema_name else name

    def updateSql(self):
        if (
            self.cboTable.currentIndex() < 0
            or len(self.tables) < self.cboTable.currentIndex()
        ):
            return

        self.table = self.tables[self.cboTable.currentIndex()]
        self.schematable = self.table.quotedName()

        self.current = self.chkCreateCurrent.isChecked()

        self.colPkey = self.db.connector.quoteId(self.editPkey.text())
        self.colStart = self.db.connector.quoteId(self.editStart.text())
        self.colEnd = self.db.connector.quoteId(self.editEnd.text())
        self.colUser = self.db.connector.quoteId(self.editUser.text())

        self.columns = [self.db.connector.quoteId(x.name) for x in self.table.fields()]

        self.colOrigPkey = None
        for constr in self.table.constraints():
            if constr.type == constr.TypePrimaryKey:
                self.origPkeyName = self.db.connector.quoteId(constr.name)
                self.colOrigPkey = [
                    self.db.connector.quoteId(x_y[1].name)
                    for x_y in iter(list(constr.fields().items()))
                ]
                break

        if self.colOrigPkey is None:
            self.txtSql.setPlainText("Table doesn't have a primary key!")
            self.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setEnabled(False)
            return
        elif len(self.colOrigPkey) > 1:
            self.txtSql.setPlainText("Table has multicolumn primary key!")
            self.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setEnabled(False)
            return

        # take first (and only column of the pkey)
        self.colOrigPkey = self.colOrigPkey[0]

        # define view, function, rule and trigger names
        self.view = self.get_escaped_name(
            self.table.schemaName(), self.table.name, "_current"
        )

        self.func_at_time = self.get_escaped_name(
            self.table.schemaName(), self.table.name, "_at_time"
        )
        self.func_update = self.get_escaped_name(
            self.table.schemaName(), self.table.name, "_update"
        )
        self.func_insert = self.get_escaped_name(
            self.table.schemaName(), self.table.name, "_insert"
        )

        self.rule_del = self.get_escaped_name(None, self.table.name, "_del")
        self.trigger_update = self.get_escaped_name(None, self.table.name, "_update")
        self.trigger_insert = self.get_escaped_name(None, self.table.name, "_insert")

        sql = []

        # modify table: add serial column, start time, end time
        sql.append(self.sql_alterTable())
        # add primary key to the table
        sql.append(self.sql_setPkey())

        sql.append(self.sql_currentView())
        # add X_at_time, X_update, X_delete functions
        sql.append(self.sql_functions())
        # add insert, update trigger, delete rule
        sql.append(self.sql_triggers())
        # add _current view + updatable
        # if self.current:
        sql.append(self.sql_updatesView())

        self.txtSql.setPlainText("\n\n".join(sql))
        self.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setEnabled(True)

        return sql

    def showHelp(self):
        helpText = """In this dialog you can set up versioning support for a table. The table will be modified so that all changes will be recorded: there will be a column with start time and end time. Every row will have its start time, end time is assigned when the feature gets deleted. When a row is modified, the original data is marked with end time and new row is created. With this system, it's possible to get back to state of the table any time in history. When selecting rows from the table, you will always have to specify at what time do you want the rows."""
        QMessageBox.information(self, "Help", helpText)

    def sql_alterTable(self):
        return f"ALTER TABLE {self.schematable} ADD {self.colPkey} serial, ADD {self.colStart} timestamp default '-infinity', ADD {self.colEnd} timestamp, ADD {self.colUser} varchar;"

    def sql_setPkey(self):
        return f"ALTER TABLE {self.schematable} DROP CONSTRAINT {self.origPkeyName}, ADD PRIMARY KEY ({self.colPkey});"

    def sql_currentView(self):
        cols = self.colPkey + "," + ",".join(self.columns)

        return (
            "CREATE VIEW %(view)s AS SELECT %(cols)s FROM %(schematable)s WHERE %(end)s IS NULL;"
            % {
                "view": self.view,
                "cols": cols,
                "schematable": self.schematable,
                "end": self.colEnd,
            }
        )

    def sql_functions(self):
        cols = ",".join(self.columns)
        all_cols = self.colPkey + "," + ",".join(self.columns)
        old_cols = ",".join("OLD." + x for x in self.columns)

        sql = f"""
CREATE OR REPLACE FUNCTION {self.func_at_time}(timestamp)
RETURNS SETOF {self.view} AS
$$
SELECT {all_cols} FROM {self.schematable} WHERE
  ( SELECT CASE WHEN {self.colEnd} IS NULL THEN ({self.colStart} <= $1) ELSE ({self.colStart} <= $1 AND {self.colEnd} > $1) END );
$$
LANGUAGE 'sql';

CREATE OR REPLACE FUNCTION {self.func_update}()
RETURNS TRIGGER AS
$$
BEGIN
  IF OLD.{self.colEnd} IS NOT NULL THEN
    RETURN NULL;
  END IF;
  IF NEW.{self.colEnd} IS NULL THEN
    INSERT INTO {self.schematable} ({cols}, {self.colStart}, {self.colEnd}) VALUES ({old_cols}, OLD.{self.colStart}, current_timestamp);
    NEW.{self.colStart} = current_timestamp;
    NEW.{self.colUser} = current_user;
  END IF;
  RETURN NEW;
END;
$$
LANGUAGE 'plpgsql';

CREATE OR REPLACE FUNCTION {self.func_insert}()
RETURNS trigger AS
$$
BEGIN
  if NEW.{self.colStart} IS NULL then
    NEW.{self.colStart} = now();
    NEW.{self.colEnd} = null;
    NEW.{self.colUser} = current_user;
  end if;
  RETURN NEW;
END;
$$
LANGUAGE 'plpgsql';"""
        return sql

    def sql_triggers(self):
        return f"""
CREATE RULE {self.rule_del} AS ON DELETE TO {self.schematable}
DO INSTEAD UPDATE {self.schematable} SET {self.colEnd} = current_timestamp WHERE {self.colPkey} = OLD.{self.colPkey} AND {self.colEnd} IS NULL;

CREATE TRIGGER {self.trigger_update} BEFORE UPDATE ON {self.schematable}
FOR EACH ROW EXECUTE PROCEDURE {self.func_update}();

CREATE TRIGGER {self.trigger_insert} BEFORE INSERT ON {self.schematable}
FOR EACH ROW EXECUTE PROCEDURE {self.func_insert}();"""

    def sql_updatesView(self):
        cols = ",".join(self.columns)
        return_cols = self.colPkey + "," + ",".join(self.columns)
        new_cols = ",".join("NEW." + x for x in self.columns)
        assign_cols = ",".join(f"{x} = NEW.{x}" for x in self.columns)

        return f"""
CREATE OR REPLACE RULE "_DELETE" AS ON DELETE TO {self.view} DO INSTEAD
  DELETE FROM {self.schematable} WHERE {self.colOrigPkey} = old.{self.colOrigPkey};
CREATE OR REPLACE RULE "_INSERT" AS ON INSERT TO {self.view} DO INSTEAD
  INSERT INTO {self.schematable} ({cols}) VALUES ({new_cols}) RETURNING {return_cols};
CREATE OR REPLACE RULE "_UPDATE" AS ON UPDATE TO {self.view} DO INSTEAD
  UPDATE {self.schematable} SET {assign_cols} WHERE {self.colOrigPkey} = NEW.{self.colOrigPkey};"""

    def onOK(self):
        # execute and commit the code
        QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor)
        try:
            sql = "\n".join(self.updateSql())
            self.db.connector._execute_and_commit(sql)

        except BaseError as e:
            DlgDbError.showError(e, self)
            return

        finally:
            QApplication.restoreOverrideCursor()

        QMessageBox.information(
            self, "DB Manager", "Versioning was successfully created."
        )
        self.accept()
