package de.ivu.elect.business.system.control;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import javax.annotation.Nullable;
import javax.enterprise.inject.Specializes;
import javax.inject.Inject;
import javax.sql.DataSource;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import de.ivu.elect.business.CurrentCustomer;
import de.ivu.elect.business.system.boundary.DbInfo;
import de.ivu.elect.business.system.entity.DbInformation;
import de.ivu.elect.business.system.entity.DbScript;
import de.ivu.elect.business.system.entity.DbType;
import de.ivu.elect.business.system.entity.SystemLogStoreEntryType;
import de.ivu.elect.business.version.boundary.AbstractVersion;
import de.ivu.elect.jee.database.DatasourceJndiResolver;
import de.ivu.elect.jee.faces.DevelopmentStore;
import de.ivu.elect.jee.startup.DbInitializer;
import de.ivu.elect.util.ManifestVersionParser;


import static de.ivu.elect.business.system.control.ElectDbInitializerSqlRunner.DB_UPDATE_TABLE_NAME;
import static de.ivu.elect.business.system.control.ElectDbInitializerSqlRunner.SCRIPT_COLUMN_NAME;
import static de.ivu.elect.business.system.control.ElectDbInitializerSqlRunner.SCRIPT_ZEITPUNKT_ERFOLGREICH_COLUMN_NAME;
import static de.ivu.elect.business.system.control.ElectDbInitializerSqlRunner.TIMESTAMP_COLUMN_NAME;
import static de.ivu.elect.business.system.entity.DbScript.CREATE_SCRIPT;
import static de.ivu.elect.business.system.entity.DbScript.VERSION_MARKER;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;

@Specializes
public class ElectDbInitializer extends DbInitializer {

  private static final Logger LOG = LoggerFactory.getLogger(ElectDbInitializer.class);

  private static final long DB_VERSION_DEVELOPMENT = 10000L; // irgendwas größer als jede jemals existierende DB-Version

  @Inject
  private transient DatasourceJndiResolver datasourceJndiResolver;

  @Inject
  private transient ElectDbInitializerSqlRunner sqlRunner;

  @Inject
  private transient ElectDbInitializerSystemLogStore logStore;

  @Inject
  private transient CurrentCustomer currentCustomer;

  @Inject
  private transient AbstractVersion version;

  @Override
  public void init() {
    LOG.info("Init elect: " + currentCustomer.getCustomer() + ", " + version.getNumber() + ", " + version.getTimestamp());
    if (Boolean.TRUE.toString().equalsIgnoreCase(System.getProperty("ivu.elect.db.update.skip"))) {
      LOG.info(getPrefix() + "Prüfe Datenbank auf Updates NICHT AKTIVIERT");
      return;
    }
    sqlRunner.checkDblock();

    LOG.info(getPrefix() + "Prüfe Datenbank auf Updates");

    var dataSource = getDataSource();
    if (dataSource == null) {
      LOG.error(getPrefix() + "Keine Datasource gefunden!");
      return;
    }

    var dbInformation = getDbInformation(dataSource);
    if (dbInformation == null) {
      LOG.error(getPrefix() + "Keine DB Informationen verfügbar!");
      DbInfo.setDbInformation(new DbInformation(false, 0L, emptyList(), "UNBEKANNT")); // NPE vermeiden
      return;
    }
    DbInfo.setDbInformation(dbInformation);
    LOG.info(getPrefix() + "DB product name: " + dbInformation.getProductName());
    var dbType = dbInformation.getDbType();
    LOG.info(getPrefix() + "DB type: " + dbType);
    LOG.info(getPrefix() + "Shema vorhanden: " + dbInformation.isSchemaVorhanden());
    var expectedDbVersion = getExpectedDbVersion();
    LOG.info(getPrefix() + "DB-Version laut Anwendung erwartet: " + expectedDbVersion);
    var bisherigeDbVersion = dbInformation.getVersion();
    LOG.info(getPrefix() + "DB-Version in der Datenbank ist: " + bisherigeDbVersion);
    LOG.info(getPrefix() + "Bisher ausgeführte Skripte: " + dbInformation.getScripts());

    if (expectedDbVersion == null) {
      LOG.error(getPrefix() + "Keine erwartete DB-Version vorhanden, breche ab.");
      return;
    }
    if (DB_VERSION_DEVELOPMENT == expectedDbVersion) {
      LOG.warn(getPrefix() + "!!! Achtung, nutze Entwicklungsversion !!!");
      logStore.addEntry(SystemLogStoreEntryType.INFO, "db.initialiazer.entwicklungsversion.verwenden");
    }
    if (dbType.isUnknown()) {
      LOG.error(getPrefix() + "Unbekanntes DBMS, breche ab.");
      return;
    }

    if (!dbInformation.isSchemaVorhanden()) {
      try {
        schemaEinspielen(dataSource, dbType);
      } catch (Exception e) {
        LOG.error(getPrefix() + "**************************************************************************************************");
        LOG.error(getPrefix() + "******** Fehler beim Einspielen der Create-Skripte ***********************************************");
        LOG.error(getPrefix() + "******** Der Fehler muss manuell analysiert und korrigiert werden(!) *****************************");
        LOG.error(getPrefix() + "******** Ein Neustart der Anwendung sollte bis zum Abschluss dieser Analys NICHT ERFOLGEN(!) *****");
        LOG.error(getPrefix() + "**************************************************************************************************");
        throw e;
      }
      logStore.addEntry(SystemLogStoreEntryType.INFO, "db.initialiazer.db.schema.eingespielt");
    }

    executeRepairSkript(dataSource, dbInformation, dbType);

    if (bisherigeDbVersion == null) {
      schreibeAktuelleVersionInDb(expectedDbVersion, dbType, dataSource);
      logStore.addEntry(SystemLogStoreEntryType.INFO, "db.initialiazer.erwartete.version.gesetzt", expectedDbVersion);
      return;
    }
    if (expectedDbVersion < bisherigeDbVersion) {
      LOG.warn(getPrefix() + "Version der Datenbank ist der erwarteten Version voraus - alte Version der Anwendung gestartet?");
      logStore.addEntry(SystemLogStoreEntryType.WARN, "db.initialiazer.erwartete.version.aelter", bisherigeDbVersion, expectedDbVersion);
      return;
    }
    if (expectedDbVersion.equals(bisherigeDbVersion)) {
      LOG.info(getPrefix() + "Version der Datenbank ist aktuell");
      return;
    }

    try {
      aktualisiereDatenbankVersion(bisherigeDbVersion, dbType, dataSource);
    } catch (Exception e) {
      LOG.error(getPrefix() + "**************************************************************************************************");
      LOG.error(getPrefix() + "******** Fehler beim Einspielen der Update-Skripte ***********************************************");
      LOG.error(getPrefix() + "******** Der Fehler muss manuell analysiert und korrigiert werden(!) *****************************");
      LOG.error(getPrefix() + "******** Ein Neustart der Anwendung sollte bis zum Abschluss dieser Analys NICHT ERFOLGEN(!) *****");
      LOG.error(getPrefix() + "**************************************************************************************************");
      throw e;
    }

  }

  /**
   * ELECT-7940
   */
  private void executeRepairSkript(DataSource dataSource, DbInformation dbInformation, DbType dbType) {
    var repairSkript = getRepairSqlSkript();

    if (StringUtils.isNotBlank(repairSkript)) {
      var repairFile = new File(repairSkript);
      if (repairFile.isFile()) {
        var repairSkriptFilename = repairFile.getName();
        if (isRepairSkriptBereitsAusgefuehrt(repairSkriptFilename, dbInformation.getScripts())) {
          LOG.info(getPrefix() + "Repair-SQL Skript bereits ausgeführt: " + repairSkript);
        } else {
          LOG.info(getPrefix() + "Repair-SQL Skript wird ausgeführt: " + repairSkript);
          try {
            var strings = getStrings(repairFile);
            if (strings.isEmpty()) {
              throw new RuntimeException("Repair-SQL Skript leer");
            }
            var ersteZeile = strings.get(0);
            if (!ersteZeile.contains(repairSkriptFilename)) {
              throw new RuntimeException(
                  "Repair-SQL Skript: Erste Zeile muss Namen der Datei in DB_UPDATE Tabelle schreiben! repairSkriptFilename: '" + repairSkriptFilename + "' nicht erkannt. Habe: " + ersteZeile);
            }
            sqlRunner.schreibeCreateScriptLockBeiFehler(dbType, repairSkript, strings, dataSource);
          } catch (SQLException e) {
            throw new RuntimeException(e.getMessage(), e);
          }
        }
      } else {
        LOG.warn("REPAIR-FILE NICHT GEFUNDEN: " + repairSkript);
      }
    }
  }

  private boolean isRepairSkriptBereitsAusgefuehrt(String repairSkript, List<DbScript> scripts) {
    return scripts.stream().anyMatch(script -> repairSkript.equalsIgnoreCase(script.getName()));
  }

  @Nullable
  private String getRepairSqlSkript() {
    var systemParameter = "ivu.elect.sql.repair." + currentCustomer.getCustomer().name();
    LOG.info(getPrefix() + "Prüfe auf gesetztes Repair-SQL Skript: '" + systemParameter + "'");
    var skript = System.getProperty(systemParameter);
    if (StringUtils.isBlank(skript)) {
      LOG.info(getPrefix() + "Kein Repair-SQL Skript angegeben");
      return null;
    }
    LOG.info(getPrefix() + "Repair-SQL Skript angegeben: '" + skript + "'");
    return skript;
  }

  private String getPrefix() {
    return currentCustomer.getPrefix();
  }

  private void aktualisiereDatenbankVersion(Long bisherigeDbVersion, DbType dbType, DataSource dataSource) {
    LOG.info(getPrefix() + "Aktualisiere Datenbank");
    var verfuegbareUpdateScripts = bestimmeVerfuegbareUpdateScripts(dbType);

    var auszufuehrendeSkripts = verfuegbareUpdateScripts.stream()
        .filter(s -> isUpdateAuszufuehren(s, bisherigeDbVersion))
        .sorted()
        .collect(Collectors.toList());

    for (var auszufuehrendeSkript : auszufuehrendeSkripts) {
      LOG.info(getPrefix() + "Führe Update-Script aus: " + auszufuehrendeSkript.getName());
      try {
        sqlRunner.schreibeCreateScriptLockBeiFehler(dbType, auszufuehrendeSkript.getName(), ladeScript(dbType, auszufuehrendeSkript.getName()), dataSource);
      } catch (SQLException e) {
        throw new RuntimeException(e.getMessage(), e);
      }
      LOG.info(getPrefix() + "Führe Update-Script aus: " + auszufuehrendeSkript.getName() + " done");
      logStore.addEntry(SystemLogStoreEntryType.INFO, "db.initialiazer.update.ausgefuehrt", auszufuehrendeSkript.getName());
    }
    LOG.info(getPrefix() + "Aktualisiere Datenbank - done");
  }

  private boolean isUpdateAuszufuehren(DbScript script, Long bisherigeDbVersion) {
    if (script.getVonVersion() == null) {
      return false;
    }
    return script.getType().isUpdateAusfuehren(bisherigeDbVersion, script.getVonVersion());
  }

  private void schreibeAktuelleVersionInDb(Long expectedDbVersion, DbType dbType, DataSource dataSource) {
    LOG.info(getPrefix() + "Keine aktuelle Versionsnummer in DB -> setze auf aktuell erwartete, führe keine Updates aus");
    try {
      sqlRunner.schreibeCreateScriptLockBeiFehler(dbType, "db_update_insert", singletonList(
          "INSERT INTO " + DB_UPDATE_TABLE_NAME + " (" +
              SCRIPT_COLUMN_NAME + ", " +
              TIMESTAMP_COLUMN_NAME + ", " +
              SCRIPT_ZEITPUNKT_ERFOLGREICH_COLUMN_NAME + ")" +
              " VALUES ('" +
              CREATE_SCRIPT + VERSION_MARKER + toVersionString(expectedDbVersion) + ".sql', " +
              dbType.getNowFunction() + ", " +
              dbType.getNowFunction() + ");"), dataSource);
      LOG.info(getPrefix() + "Aktuelle Versionsnummer gesetzt: " + expectedDbVersion);
    } catch (Exception e) {
      LOG.error(getPrefix() + "Fehler beim Schreiben der aktuellen Versionsnummer in die Datenbank: " + e.getMessage(), e);
    }
  }

  private String toVersionString(Long expectedDbVersion) {
    var result = expectedDbVersion.toString();
    result = StringUtils.leftPad(result, 3, '0');
    return result;
  }

  private void schemaEinspielen(DataSource dataSource, DbType dbType) {
    LOG.info(getPrefix() + "Kein DB Schema vorhanden, lege an");

    // Create Script
    var sqlCreateScript = ladeCreateScript(dbType);
    if (sqlCreateScript.isEmpty()) {
      LOG.error(getPrefix() + "Kein create-script gefunden");
      return;
    }
    try {
      sqlRunner.schreibeCreateScriptLockBeiFehler(dbType, "CREATE-SCRIPT", sqlCreateScript, dataSource);
    } catch (SQLException e) {
      throw new RuntimeException(e.getMessage(), e);
    }

    // Indexe
    LOG.info(getPrefix() + "DB Schema angelegt - aktualisiere Indexe");
    try {
      sqlRunner.schreibeCreateScriptLockBeiFehler(dbType, "UPDATE-INDEX", ladeUpdateIndexScript(dbType), dataSource);
    } catch (SQLException e) {
      throw new RuntimeException(e.getMessage(), e);
    }

    LOG.info(getPrefix() + "DB Schema vollständig");
  }

  private List<String> ladeUpdateIndexScript(DbType dbType) {
    return ladeScript(dbType, "create-index-wvz.sql");
  }

  private List<String> ladeCreateScript(DbType dbType) {
    return ladeScript(dbType, "elect-create-all.sql");
  }

  private List<DbScript> bestimmeVerfuegbareUpdateScripts(DbType dbType) {

    List<DbScript> result = new ArrayList<>();
    try {
      try (var is = Thread.currentThread().getContextClassLoader().getResourceAsStream(dbType.getSqlResouceName())) {
        try (var zis = new ZipInputStream(is)) {
          ZipEntry entry;
          while ((entry = zis.getNextEntry()) != null) {
            if (entry.getName().startsWith("update-scripte")) {
              result.add(new DbScript(entry.getName()));
            }
          }
        }
      }
    } catch (Exception e) {
      LOG.error(getPrefix() + "Fehler bei Laden der verfügbaaren Update-Scripte: " + e.getMessage(), e);
    }
    return result;

  }

  private List<String> ladeScript(DbType dbType, String scriptName) {
    try {
      try (var is = Thread.currentThread().getContextClassLoader().getResourceAsStream(dbType.getSqlResouceName())) {
        try (var zis = new ZipInputStream(is)) {
          ZipEntry entry;
          while ((entry = zis.getNextEntry()) != null) {
            if (scriptName.equalsIgnoreCase(entry.getName())) {
              return getStrings(zis);
            }
          }
        }
      }
    } catch (Exception e) {
      LOG.error(getPrefix() + "Fehler bei Laden des SQL-Scriptes: " + scriptName + ", " + e.getMessage() + "; dbType=" + dbType, e);
    }
    return emptyList();
  }

  private static List<String> getStrings(File f) {
    try (var fis = new FileInputStream(f)) {
      return getStrings(fis);
    } catch (IOException e) {
      throw new RuntimeException(e.getMessage(), e);
    }
  }

  public static List<String> getStrings(InputStream is) throws IOException {
    try (var buffer = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
      return buffer.lines().collect(Collectors.toList());
    }
  }

  @Nullable
  private DbInformation getDbInformation(DataSource dataSource) {
    try {
      LOG.info(getPrefix() + "Datasource gefunden: " + dataSource);
      return sqlRunner.getDbVersion(dataSource);
    } catch (Exception e) {
      LOG.error(getPrefix() + "Fehler beim Bestimmen der Datenbank-Informationen: " + e.getMessage(), e);
      return null;
    }
  }

  @Nullable
  private DataSource getDataSource() {
    try {
      return datasourceJndiResolver.getDatasource();
    } catch (Exception e) {
      LOG.error(getPrefix() + "Fehler beim Bestimmen der Datasource: " + e.getMessage(), e);
      return null;
    }
  }

  @Nullable
  private Long getExpectedDbVersion() {
    String version = null;
    try {
      var parser = new ManifestVersionParser(getClass(), AbstractVersion.ELECT_GROUP_ID, "elect-model-db");
      version = parser.getVersion();
      if (version == null) {
        return null;
      }

      if (isDevelopmentSnapshotVersion(version)) {
        // HINT: Zum Entwickeln tests mit Snapshot-Version von elect-model zulassen
        return DB_VERSION_DEVELOPMENT;
      }

      var start = version.indexOf(".");
      if (start > -1) {
        return Long.parseLong(version.substring(start + 1, start + 4));
      }
    } catch (Exception e) {
      LOG.error(getPrefix() + "Fehler beim Bestimmen der erwarteten DB-Version: '" + version + "', " + e.getMessage(), e);
    }
    return null;

  }

  /**
   * HINT: erlaubt es, in der elect/pom.xml <code>10.0.0-SNAPSHOT</code> als Datenbank-Version einzutragen, und die
   * Änderungen ohne Release zu testen.
   */
  private boolean isDevelopmentSnapshotVersion(String version) {
    return DevelopmentStore.isDevelopmentStageSet() && "10.0.0-SNAPSHOT".equals(version);
  }
}
