package org.folio.rest.persist.ddlgen;

import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import org.folio.rest.tools.PomReader;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import freemarker.template.TemplateExceptionHandler;
import freemarker.template.Version;

/**
 * @author shale
 *
 */
public class SchemaMaker {

  private static Configuration cfg;
  private Map<String, Object> templateInput = new HashMap<>();
  private String tenant;
  private String module;
  private TenantOperation mode;
  private String previousVersion;
  private String newVersion;
  private String rmbVersion;
  private Schema schema;
  private Schema previousSchema;
  private String schemaJson = "{}";

  /**
   * @param onTable
   */
  public SchemaMaker(String tenant, String module, TenantOperation mode, String previousVersion, String newVersion){
    if(SchemaMaker.cfg == null){
      //do this ONLY ONCE
      SchemaMaker.cfg = new Configuration(new Version(2, 3, 26));
      // Where do we load the templates from:
      cfg.setClassForTemplateLoading(SchemaMaker.class, "/templates/db_scripts");
      cfg.setDefaultEncoding("UTF-8");
      cfg.setLocale(Locale.US);
      cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
    }
    this.tenant = tenant;
    this.module = module;
    this.mode = mode;
    this.previousVersion = previousVersion;
    this.newVersion = newVersion;
    this.rmbVersion = PomReader.INSTANCE.getRmbVersion();
  }

  public String generateDDL() throws IOException, TemplateException {
    return generateDDL(false);
  }

  public String generateDDL(boolean recreateIndexMode) throws IOException, TemplateException {

    templateInput.put("myuniversity", this.tenant);

    templateInput.put("mymodule", this.module);

    templateInput.put("mode", this.mode);

    if("delete".equalsIgnoreCase(this.mode.name())){
      return handleDelete();
    }

    if(this.schema == null){
      //log this
      System.out.print("Must call setSchema() first...");
      return null;
    }

    String pVersion = this.previousVersion;

    if(pVersion == null){
      //will be null on deletes unless its read from db by rmb
      pVersion = "0.0";
    }
    if(newVersion == null){
      newVersion = "0.0";
    }

    templateInput.put("version", pVersion);

    templateInput.put("newVersion", this.newVersion);

    //TODO - check the rmbVersion in the internal_rmb table and compare to this passed in
    //version, to check if core rmb scripts need updating due to an update
    templateInput.put("rmbVersion", this.rmbVersion);

    templateInput.put("schemaJson", this.getSchemaJson());

    this.schema.setup();

    templateInput.put("tables", tables());

    templateInput.put("views", this.schema.getViews());

    templateInput.put("scripts", this.schema.getScripts());

    templateInput.put("exactCount", this.schema.getExactCount()+"");

    String template = "main.ftl";
    if(recreateIndexMode){
      template = "indexes_only.ftl";
    }
    Template tableTemplate = cfg.getTemplate(template);
    Writer writer = new StringWriter();
    tableTemplate.process(templateInput, writer);

    return writer.toString();
  }

  private String handleDelete() throws IOException, TemplateException {
    Writer writer = new StringWriter();
    Template tableTemplate = cfg.getTemplate("delete.ftl");
    tableTemplate.process(templateInput, writer);
    return writer.toString();
  }

  /**
   * @return fieldName is the same and not null, or fieldPath is the same and not null
   */
  static boolean sameForeignKey(ForeignKeys a, ForeignKeys b) {
    return (a.getFieldName() != null && a.getFieldName().equals(b.getFieldName())) ||
        (a.getFieldPath() != null && a.getFieldPath().equals(b.getFieldPath()));
  }

  /**
   * @return the tables of schema plus tables to delete (tables that exist in previousSchema but not in schema);
   *   add foreign keys to delete (those that exist in previousSchema but not in schema).
   *   Nothing to do for indexes because rmb_internal_index handles them.
   */
  List<Table> tables() {
    if (previousSchema == null || previousSchema.getTables() == null) {
      return schema.getTables();
    }
    Map<String,Table> tableForName = new HashMap<>();
    schema.getTables().forEach(table -> tableForName.put(table.getTableName(), table));
    List<Table> list = new ArrayList<>(schema.getTables());
    previousSchema.getTables().forEach(oldTable -> {
      Table newTable = tableForName.get(oldTable.getTableName());
      if (newTable == null) {
        oldTable.setMode("delete");
        oldTable.setup();
        list.add(oldTable);
        return;
      }

      List<ForeignKeys> oldForeignKeys = oldTable.getForeignKeys();
      if (oldForeignKeys == null || oldForeignKeys.isEmpty()) {
        return;
      }
      List<ForeignKeys> newForeignKeys =
          newTable.getForeignKeys() == null ? Collections.emptyList() : newTable.getForeignKeys();
      List<ForeignKeys> allForeignKeys = new ArrayList<>(newForeignKeys);
      oldForeignKeys.forEach(oldForeignKey -> {
        if (newForeignKeys.stream()
            .anyMatch(newForeignKey -> sameForeignKey(oldForeignKey, newForeignKey))) {
          // an entry for oldForeignKey exists in newForeignKeys, nothing to do
          return;
        }
        oldForeignKey.settOps(TableOperation.DELETE);
        oldForeignKey.setup();
        allForeignKeys.add(oldForeignKey);
      });
      newTable.setForeignKeys(allForeignKeys);
    });
    return list;
  }

  public String getTenant() {
    return tenant;
  }

  public void setTenant(String tenant) {
    this.tenant = tenant;
  }

  public String getModule() {
    return module;
  }

  public void setModule(String module) {
    this.module = module;
  }

  public TenantOperation getMode() {
    return mode;
  }

  public void setMode(TenantOperation mode) {
    this.mode = mode;
  }

  public Schema getSchema() {
    return schema;
  }

  public void setSchema(Schema schema) {
    this.schema = schema;
  }

  public Schema getPreviousSchema() {
    return previousSchema;
  }

  public void setPreviousSchema(Schema previousSchema) {
    this.previousSchema = previousSchema;
  }

  public String getSchemaJson() {
    return schemaJson;
  }

  public void setSchemaJson(String schemaJson) {
    this.schemaJson = schemaJson;
  }
}