package su.xserver.iikocon.test;

import io.vertx.core.Future;
import io.vertx.core.Promise;
import io.vertx.core.Vertx;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.client.WebClient;
import io.vertx.ext.web.client.WebClientOptions;
import io.vertx.mysqlclient.MySQLConnectOptions;
import io.vertx.sqlclient.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HexFormat;
import java.util.List;

public class IikoOlapColumnsImporter {

  private static final Logger log = LoggerFactory.getLogger(IikoOlapColumnsImporter.class);
  private final WebClient httpClient;
  private final Pool dbPool;
  private final String iikoServer;
  private final String iikoLogin;
  private final String iikoPassword;
  private static long time;

  private static final List<String> REPORT_TYPES = List.of("SALES", "TRANSACTIONS", "DELIVERIES");

  public IikoOlapColumnsImporter(Vertx vertx,
                                 String iikoServer,
                                 String iikoLogin,
                                 String iikoPassword,
                                 String dbHost, int dbPort,
                                 String dbName, String dbUser, String dbPassword) {
    WebClientOptions options = new WebClientOptions()
      .setSsl(true)
      .setTrustAll(true)
      .setVerifyHost(false);
    this.httpClient = WebClient.create(vertx, options);
    this.iikoServer = iikoServer;
    this.iikoLogin = iikoLogin;
    this.iikoPassword = iikoPassword;

    MySQLConnectOptions connectOptions = new MySQLConnectOptions()
      .setHost(dbHost)
      .setPort(dbPort)
      .setDatabase(dbName)
      .setUser(dbUser)
      .setPassword(dbPassword)
      .setCharset("utf8mb4");
    PoolOptions poolOptions = new PoolOptions().setMaxSize(5);
    this.dbPool = Pool.pool(vertx, connectOptions, poolOptions);
  }

  // Главный метод: последовательно для каждого reportType делаем auth -> fetch -> store -> logout
  public Future<Void> fetchAndStoreAll() {
    return createTablesIfNotExist()
      .compose(v -> processAllReportTypesSequentially())
      .onSuccess(v -> log.info("All reports imported successfully"))
      .onFailure(err -> log.error("Import failed: {}", err.getMessage()));
  }

  private Future<Void> processAllReportTypesSequentially() {
    time = System.currentTimeMillis();
    Future<Void> result = Future.succeededFuture();
    for (String reportType : REPORT_TYPES) {
      result = result.compose(v -> processOneReportType(reportType));
    }
    return result;
  }

  private Future<Void> processOneReportType(String reportType) {
    log.info("Processing report type: {}", reportType);
    return authenticate()
      .compose(token -> {
        return fetchColumnsFromIiko(reportType, token)
          .compose(columnsJson -> storeColumnsToDb(reportType, columnsJson))
          .onComplete(ignored -> logout(token)); // logout всегда, даже при ошибке
      });
  }

  // Аутентификация: GET /resto/api/auth?login=...&pass=SHA1
  private Future<String> authenticate() {
    Promise<String> promise = Promise.promise();
    String passHash = sha1(iikoPassword);
    String url = "https://" + iikoServer + ":443/resto/api/auth?login=" + iikoLogin + "&pass=" + passHash;

    httpClient.getAbs(url)
      .send()
      .onSuccess(resp -> {
        if (resp.statusCode() == 200) {
          String token = resp.bodyAsString();
          log.info("Authenticated, token: {}", token);
          promise.complete(token);
        } else {
          promise.fail("Auth failed for " + iikoLogin + ": " + resp.statusCode());
        }
      })
      .onFailure(promise::fail);
    return promise.future();
  }

  // Logout: GET /resto/api/logout?key=токен
  private Future<Void> logout(String token) {
    if (token == null || token.isEmpty()) {
      return Future.succeededFuture();
    }
    Promise<Void> promise = Promise.promise();
    String url = "https://" + iikoServer + "/resto/api/logout?key=" + token;
    httpClient.getAbs(url)
      .send()
      .onSuccess(resp -> {
        log.info("Logout completed for token, status {}", resp.statusCode());
        promise.complete();
      })
      .onFailure(err -> {
        log.error("Logout request failed: {}", err.getMessage());
        promise.complete(); // не ломаем цепочку
      });
    return promise.future();
  }

  // Запрос полей для конкретного reportType
  private Future<JsonObject> fetchColumnsFromIiko(String reportType, String token) {
    Promise<JsonObject> promise = Promise.promise();
    String url = "https://" + iikoServer + "/resto/api/v2/reports/olap/columns?key=" + token + "&reportType=" + reportType;

    log.info("Connect to : {}", url);

    httpClient.getAbs(url)
      .send()
      .onSuccess(resp -> {
        if (resp.statusCode() == 200) {
          JsonObject body = resp.bodyAsJsonObject();
          // Если есть обёртка data, распаковываем
          JsonObject data = body.containsKey("data") && body.getValue("data") instanceof JsonObject
            ? body.getJsonObject("data")
            : body;
          promise.complete(data);

          log.info("time: {}", (System.currentTimeMillis() - time) + "ms");
        } else {
          promise.fail("Failed to fetch columns for " + reportType + ": HTTP " + resp.statusCode());
        }
      })
      .onFailure(promise::fail);
    return promise.future();
  }

  // SHA-1
  private String sha1(String input) {
    try {
      MessageDigest md = MessageDigest.getInstance("SHA-1");
      byte[] digest = md.digest(input.getBytes());
      return HexFormat.of().formatHex(digest).toLowerCase();
    } catch (NoSuchAlgorithmException e) {
      throw new RuntimeException(e);
    }
  }

  // ---------- Методы работы с БД (с префиксом iiko_) ----------
  private Future<Void> storeColumnsToDb(String reportType, JsonObject columns) {
    return getOrCreateReportType(reportType)
      .compose(reportTypeId -> {
        List<Future<Void>> fieldFutures = new ArrayList<>();
        for (String fieldKey : columns.fieldNames()) {
          JsonObject fieldDef = columns.getJsonObject(fieldKey);
          fieldFutures.add(storeSingleField(reportTypeId, fieldKey, fieldDef));
        }
        return Future.all(fieldFutures).mapEmpty();
      });
  }

  private Future<Integer> getOrCreateReportType(String reportType) {
    Promise<Integer> promise = Promise.promise();
    String selectSql = "SELECT report_type_id FROM iiko_report_types WHERE name = ?";
    dbPool.preparedQuery(selectSql)
      .execute(Tuple.of(reportType))
      .onComplete(ar -> {
        if (ar.succeeded() && ar.result().size() > 0) {
          promise.complete(ar.result().iterator().next().getInteger("report_type_id"));
        } else if (ar.succeeded()) {
          String insertSql = "INSERT INTO iiko_report_types (name, description) VALUES (?, ?)";
          dbPool.preparedQuery(insertSql)
            .execute(Tuple.of(reportType, "OLAP report type: " + reportType))
            .onComplete(insAr -> {
              if (insAr.succeeded()) {
                dbPool.preparedQuery(selectSql)
                  .execute(Tuple.of(reportType))
                  .onComplete(selAr -> {
                    if (selAr.succeeded() && selAr.result().size() > 0) {
                      promise.complete(selAr.result().iterator().next().getInteger("report_type_id"));
                    } else {
                      promise.fail("Cannot retrieve inserted report_type_id for " + reportType);
                    }
                  });
              } else {
                promise.fail(insAr.cause());
              }
            });
        } else {
          promise.fail(ar.cause());
        }
      });
    return promise.future();
  }

  private Future<Void> storeSingleField(int reportTypeId, String fieldKey, JsonObject fieldDef) {
    // Нормализованный ключ (без точек)
    String fieldKeyNormal = fieldKey.replace('.', '_');

    String name = fieldDef.getString("name");
    String originalType = fieldDef.getString("type");
    String typeNormal = normalizeType(originalType);

    boolean aggregationAllowed = fieldDef.getBoolean("aggregationAllowed", false);
    boolean groupingAllowed = fieldDef.getBoolean("groupingAllowed", false);
    boolean filteringAllowed = fieldDef.getBoolean("filteringAllowed", false);
    JsonArray tagsArray = fieldDef.getJsonArray("tags", new JsonArray());

    String insertFieldSql = """
        INSERT INTO iiko_fields (
            report_type_id, field_key, field_key_normal, name, type, type_normal,
            aggregation_allowed, grouping_allowed, filtering_allowed
        )
        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
        ON DUPLICATE KEY UPDATE
            field_key_normal = VALUES(field_key_normal),
            name = VALUES(name),
            type_normal = VALUES(type_normal),
            aggregation_allowed = VALUES(aggregation_allowed),
            grouping_allowed = VALUES(grouping_allowed),
            filtering_allowed = VALUES(filtering_allowed)
        """;

    return dbPool.preparedQuery(insertFieldSql)
      .execute(Tuple.of(
        reportTypeId, fieldKey, fieldKeyNormal, name, originalType, typeNormal,
        aggregationAllowed, groupingAllowed, filteringAllowed
      ))
      .compose(ignored -> {
        String selectFieldIdSql = "SELECT field_id FROM iiko_fields WHERE report_type_id = ? AND field_key = ?";
        return dbPool.preparedQuery(selectFieldIdSql)
          .execute(Tuple.of(reportTypeId, fieldKey))
          .compose(rows -> {
            if (rows.size() == 0) {
              return Future.failedFuture("Field not found after upsert: " + fieldKey);
            }
            int fieldId = rows.iterator().next().getInteger("field_id");
            return processTags(fieldId, tagsArray);
          });
      });
  }

  private Future<Void> processTags(int fieldId, JsonArray tags) {
    List<Future<Void>> tagFutures = new ArrayList<>();
    for (Object tagObj : tags) {
      String tagName = tagObj.toString();
      tagFutures.add(getOrCreateTag(tagName)
        .compose(tagId -> linkFieldTag(fieldId, tagId)));
    }
    return Future.all(tagFutures).mapEmpty();
  }

  private Future<Integer> getOrCreateTag(String tagName) {
    Promise<Integer> promise = Promise.promise();
    String selectSql = "SELECT tag_id FROM iiko_tags WHERE tag_name = ?";
    dbPool.preparedQuery(selectSql)
      .execute(Tuple.of(tagName))
      .onComplete(ar -> {
        if (ar.succeeded() && ar.result().size() > 0) {
          promise.complete(ar.result().iterator().next().getInteger("tag_id"));
        } else {
          String insertSql = "INSERT IGNORE INTO iiko_tags (tag_name) VALUES (?)";
          dbPool.preparedQuery(insertSql)
            .execute(Tuple.of(tagName))
            .onComplete(insAr -> {
              // После IGNORE всё равно выбираем ID (он мог уже существовать)
              dbPool.preparedQuery(selectSql)
                .execute(Tuple.of(tagName))
                .onComplete(selAr -> {
                  if (selAr.succeeded() && selAr.result().size() > 0) {
                    promise.complete(selAr.result().iterator().next().getInteger("tag_id"));
                  } else {
                    promise.fail("Cannot retrieve tag_id for " + tagName);
                  }
                });
            });
        }
      });
    return promise.future();
  }

  private Future<Void> linkFieldTag(int fieldId, int tagId) {
    String sql = "INSERT IGNORE INTO iiko_field_tags (field_id, tag_id) VALUES (?, ?)";
    return dbPool.preparedQuery(sql)
      .execute(Tuple.of(fieldId, tagId))
      .mapEmpty();
  }

  private Future<Void> createTablesIfNotExist() {
    String createReportTypesTable = """
      CREATE TABLE IF NOT EXISTS iiko_report_types (
          report_type_id INT AUTO_INCREMENT PRIMARY KEY,
          name VARCHAR(50) UNIQUE NOT NULL,
          description TEXT NOT NULL
      ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
      """;
    String createFieldsTable = """
      CREATE TABLE IF NOT EXISTS iiko_fields (
          field_id INT AUTO_INCREMENT PRIMARY KEY,
          report_type_id INT NOT NULL,
          field_key VARCHAR(255) NOT NULL,
          field_key_normal VARCHAR(255) NOT NULL,
          name VARCHAR(255) NOT NULL,
          type VARCHAR(50) NOT NULL,
          type_normal VARCHAR(50) NOT NULL,
          aggregation_allowed BOOLEAN NOT NULL DEFAULT 0,
          grouping_allowed BOOLEAN NOT NULL DEFAULT 0,
          filtering_allowed BOOLEAN NOT NULL DEFAULT 0,
          created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
          UNIQUE KEY uk_fields_report_type_field_key (report_type_id, field_key),
          FOREIGN KEY (report_type_id) REFERENCES iiko_report_types(report_type_id) ON DELETE RESTRICT
      ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
      """;
    String createTagsTable = """
      CREATE TABLE IF NOT EXISTS iiko_tags (
          tag_id INT AUTO_INCREMENT PRIMARY KEY,
          tag_name VARCHAR(100) UNIQUE NOT NULL
      ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
      """;
    String createFieldTagsTable = """
      CREATE TABLE IF NOT EXISTS iiko_field_tags (
          field_id INT NOT NULL,
          tag_id INT NOT NULL,
          PRIMARY KEY (field_id, tag_id),
          FOREIGN KEY (field_id) REFERENCES iiko_fields(field_id) ON DELETE CASCADE,
          FOREIGN KEY (tag_id) REFERENCES iiko_tags(tag_id) ON DELETE CASCADE
      ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
      """;
    String createIdxFieldsReportType = "CREATE INDEX IF NOT EXISTS idx_fields_report_type ON iiko_fields(report_type_id)";
    String createIdxFieldsName = "CREATE INDEX IF NOT EXISTS idx_fields_name ON iiko_fields(name)";
    String createIdxFieldTagsTagId = "CREATE INDEX IF NOT EXISTS idx_field_tags_tag_id ON iiko_field_tags(tag_id)";

    return dbPool.query(createReportTypesTable).execute()
      .compose(ignored -> dbPool.query(createFieldsTable).execute())
      .compose(ignored -> dbPool.query(createTagsTable).execute())
      .compose(ignored -> dbPool.query(createFieldTagsTable).execute())
      .compose(ignored -> dbPool.query(createIdxFieldsReportType).execute())
      .compose(ignored -> dbPool.query(createIdxFieldsName).execute())
      .compose(ignored -> dbPool.query(createIdxFieldTagsTagId).execute())
      .mapEmpty();
  }

  private String normalizeType(String iikoType) {
    if (iikoType == null) return "string";
    return switch (iikoType) {
      case "ENUM", "STRING", "ID", "ID_STRING" -> "string";
      case "DATETIME" -> "datetime";
      case "INTEGER", "DURATION_IN_SECONDS" -> "integer";
      case "PERCENT", "AMOUNT", "MONEY" -> "decimal";
      default -> "string";
    };
  }

  public void close() {
    dbPool.close();
    httpClient.close();
  }
}
