package old.util;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;

import com.zaxxer.hikari.HikariDataSource;
import org.jooq.DSLContext;
import org.jooq.Table;
import org.jooq.TableField;
import org.jooq.Record;
import org.jooq.conf.RenderNameCase;
import org.jooq.conf.Settings;
import org.jooq.impl.DSL;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ru.cg.webbpm.modules.database.bean.config.Datasource;
import ru.cg.webbpm.modules.database.impl.data_source.HikariDataSourceFactory;

import static ru.cg.webbpm.modules.security.api.impl.bean.Tables.LINK_USER_ACCOUNT_USER_GROUP;
import static ru.cg.webbpm.modules.security.api.impl.bean.Tables.LINK_USER_GROUP_USER_ROLE;
import static ru.cg.webbpm.modules.security.api.impl.bean.Tables.LINK_USER_ROLE_AUTHORITY;
import static ru.cg.webbpm.modules.security.api.impl.bean.Tables.ORG_UNIT;
import static ru.cg.webbpm.modules.security.api.impl.bean.Tables.SIMPLE_CREDENTIALS;
import static ru.cg.webbpm.modules.security.api.impl.bean.Tables.USER_ACCOUNT;
import static ru.cg.webbpm.modules.security.api.impl.bean.Tables.USER_ACCOUNT_VERIFICATION;
import static ru.cg.webbpm.modules.security.api.impl.bean.Tables.USER_GROUP;
import static ru.cg.webbpm.modules.security.api.impl.bean.Tables.USER_ROLE;

/**
 * @author gulnaz
 */
public class ExecutionManager {
  private static final Logger LOGGER = LoggerFactory.getLogger(ExecutionManager.class);
  private static final int POOL_SIZE = 2;

  private HikariDataSource testDataSource;
  public HikariDataSource securityDataSource;
  private DSLContext dslContext;
  private Settings settings;

  public ExecutionManager() {
    LOGGER.info("Execution manager creation");
    HikariDataSourceFactory hikariDataSourceFactory = new HikariDataSourceFactory(POOL_SIZE);

    testDataSource = hikariDataSourceFactory.createDataSource(getDataSource(DataSourceName.TEST));

    securityDataSource = hikariDataSourceFactory.createDataSource(getDataSource(DataSourceName.SECURITY));
    securityDataSource.setLeakDetectionThreshold(60000);

    settings = new Settings();
    settings.setRenderNameCase(RenderNameCase.LOWER);

    //TODO enable and change wrong port to right
//    jbpmDataSource = hikariDataSourceFactory.createDataSource(getDataSource(DataSourceName.JBPM));
//    jbpmDataSource.setMaximumPoolSize(5);
  }

  public enum DataSourceName {
    JBPM,
    SECURITY,
    TEST
  }

  public String generateId() {
    return UUID.randomUUID().toString();
  }

  private Datasource getDataSource(DataSourceName dataSourceName) {
    String host = System.getenv("HOSTNAME");
    String databaseName;
    String user;
    String password;
    Datasource datasource = new Datasource();

    switch (dataSourceName) {
      case JBPM:
        databaseName = System.getenv("DB_JBPM_NAME");
        user = System.getenv("DB_JBPM_USERNAME");
        password = System.getenv("DB_JBPM_PASSWORD");
        break;
      case SECURITY:
        host = System.getenv("DB_SEC_HOST");
        databaseName = System.getenv("DB_SEC_NAME");
        user = System.getenv("DB_SEC_USERNAME");
        password = System.getenv("DB_SEC_PASSWORD");
        break;
      case TEST:
        host = System.getenv("DB_APP_HOST");
        databaseName = System.getenv("DB_APP_NAME");
        user = System.getenv("DB_APP_USERNAME");
        password = System.getenv("DB_APP_PASSWORD");
        break;
      default:
        throw new IllegalStateException("Unexpected value: " + dataSourceName);
    }

    datasource.setUrl(String.format("jdbc:postgresql://%s:%s/%s",
        host,
        System.getenv("DB_PORT"),
        databaseName));
    datasource.setUser(user);
    datasource.setPassword(password);
    datasource.setDriverClassName("org.postgresql.Driver");

    return datasource;
  }

  public <R extends Record> int countTableRows(Table<R> table) {
    try (Connection connection = testDataSource.getConnection()) {
      dslContext = DSL.using(connection);
      return dslContext.selectCount()
          .from(table)
          .fetchOne(0, int.class);
    }
    catch (SQLException e) {
      throw new RuntimeException("Failed to count rows", e);
    }
  }

  public List<String> getGroupNamesFromUserGroup() {
    try (Connection connection = securityDataSource.getConnection()) {
      dslContext = DSL.using(connection, settings);

      return dslContext.select()
          .from(USER_GROUP)
          .fetch()
          .getValues(USER_GROUP.NAME);
    }
    catch (SQLException e) {
      throw new RuntimeException("Failed to get group names from user group table", e);
    }
  }

  public String getTokenByUserId(String id) {
    try (Connection connection = securityDataSource.getConnection()) {
      dslContext = DSL.using(connection, settings);

      return dslContext.select()
          .from(USER_ACCOUNT_VERIFICATION)
          .where(USER_ACCOUNT_VERIFICATION.USER_ACCOUNT_ID.eq(id))
          .fetch()
          .getValue(0, USER_ACCOUNT_VERIFICATION.TOKEN);
    }
    catch (SQLException e) {
      throw new RuntimeException("Failed to get token from user_account_verification", e);
    }
  }

  public List<String> getRoleNamesFromUserRole() {
    try (Connection connection = securityDataSource.getConnection()) {
      dslContext = DSL.using(connection, settings);

      return dslContext.select()
          .from(USER_ROLE)
          .fetch()
          .getValues(USER_ROLE.NAME);
    }
    catch (SQLException e) {
      throw new RuntimeException("Failed to get roles from user_role table", e);
    }
  }

  public String getUserAccountIdByName(String userName) {
    try (Connection connection = securityDataSource.getConnection()) {
      dslContext = DSL.using(connection, settings);

      return dslContext.select()
          .from(USER_ACCOUNT)
          .where(USER_ACCOUNT.FIRST_NAME.eq(userName))
          .fetch()
          .getValue(0, USER_ACCOUNT.USER_ACCOUNT_ID);
    }
    catch (SQLException e) {
      throw new RuntimeException("Failed to get user_account_id from user_account table", e);
    }
  }

  public String getRoleIdByName(String roleName) {
    try (Connection connection = securityDataSource.getConnection()) {
      dslContext = DSL.using(connection, settings);

      return dslContext.select()
          .from(USER_ROLE)
          .where(USER_ROLE.NAME.eq(roleName))
          .fetch()
          .getValue(0, USER_ROLE.USER_ROLE_ID);
    }
    catch (SQLException e) {
      throw new RuntimeException("Failed to get user_role_id from user_role table", e);
    }
  }

  public String getGroupIdByName(String groupName) {
    try (Connection connection = securityDataSource.getConnection()) {
      dslContext = DSL.using(connection, settings);

      return dslContext.select()
          .from(USER_GROUP)
          .where(USER_GROUP.NAME.eq(groupName))
          .fetch()
          .getValue(0, USER_GROUP.USER_GROUP_ID);
    }
    catch (SQLException e) {
      throw new RuntimeException("Failed to get user_group_id from user_group table", e);
    }
  }

  /*
  Queries with security datasource
  */
  private <R extends Record> void insertSecurityRecord(Table<R> table,
      TableField<R, String> field1, TableField<R, String> field2, TableField<R, String> field3,
      String value1, String value2, String value3) {
    try (Connection connection = securityDataSource.getConnection()) {
      dslContext = DSL.using(connection, settings);
      dslContext.insertInto(table, field1, field2, field3)
          .values(value1, value2, value3)
          .execute();
    }
    catch (SQLException e) {
      throw new RuntimeException("Failed to delete record", e);
    }
  }

  private <R extends Record> void deleteSecurityRecord(Table<R> table, TableField<R, String> field, String value) {
    try (Connection connection = securityDataSource.getConnection()) {
      dslContext = DSL.using(connection, settings);
      dslContext.delete(table)
          .where(field.eq(value))
          .execute();
    }
    catch (SQLException e) {
      throw new RuntimeException("Failed to delete record", e);
    }
  }

  public void insertUser(String username, String password, String email, String orgId, String groupId) {
    //TODO SUPPORT-3761
    try (Connection connection = securityDataSource.getConnection()) {
      String userId = generateId();
      dslContext = DSL.using(connection, settings);
      dslContext.batch(
              dslContext
                  .insertInto(USER_ACCOUNT,
                      USER_ACCOUNT.USER_ACCOUNT_ID, USER_ACCOUNT.USERNAME, USER_ACCOUNT.EMAIL,
                      USER_ACCOUNT.FIRST_NAME, USER_ACCOUNT.LAST_NAME, USER_ACCOUNT.ORG_UNIT_ID)
                  .values(userId, username, email, username, username, orgId),
              dslContext
                  .insertInto(SIMPLE_CREDENTIALS,
                      SIMPLE_CREDENTIALS.USER_ACCOUNT_ID, SIMPLE_CREDENTIALS.PASSWORD)
                  .values(userId, password),
              dslContext.insertInto(LINK_USER_ACCOUNT_USER_GROUP, LINK_USER_ACCOUNT_USER_GROUP.LINK_USER_ACCOUNT_USER_GROUP_ID,
                      LINK_USER_ACCOUNT_USER_GROUP.USER_ACCOUNT_ID, LINK_USER_ACCOUNT_USER_GROUP.USER_GROUP_ID)
                  .values(generateId(), userId, groupId))
          .execute();
    }
    catch (SQLException e) {
      throw new RuntimeException("Failed to insert user", e);
    }
  }

  public void deleteTestOrg(String orgName) {
    deleteSecurityRecord(ORG_UNIT, ORG_UNIT.NAME, orgName);
  }

  public void createTestOrg(String orgName) {
    insertSecurityRecord(ORG_UNIT, ORG_UNIT.ID, ORG_UNIT.NAME, ORG_UNIT.CODE, generateId(), orgName, orgName);
  }

  public void createParentAndChildOrgs(String orgName1, String orgName2) {
    try (Connection connection = securityDataSource.getConnection()) {
      String parentOrgId = generateId();
      dslContext = DSL.using(connection, settings);
      dslContext.batch(
              dslContext.insertInto(ORG_UNIT, ORG_UNIT.ID, ORG_UNIT.NAME, ORG_UNIT.CODE)
                  .values(parentOrgId, orgName1, orgName1),
              dslContext.insertInto(ORG_UNIT, ORG_UNIT.ID, ORG_UNIT.NAME, ORG_UNIT.CODE, ORG_UNIT.PARENT_ID)
                  .values(generateId(), orgName2, orgName2, parentOrgId))
          .execute();
    }
    catch (SQLException e) {
      throw new RuntimeException("Failed to create organizations", e);
    }
  }

  public void deleteTestUser(String userName) {
    deleteSecurityRecord(USER_ACCOUNT, USER_ACCOUNT.FIRST_NAME, userName);
  }

  public void createUser(String username, String organizationID) {
    String userAccountId = generateId();
    try (Connection connection = securityDataSource.getConnection()) {
      dslContext = DSL.using(connection, settings);
      dslContext.batch(
              dslContext
                  .insertInto(USER_ACCOUNT,
                      USER_ACCOUNT.USER_ACCOUNT_ID, USER_ACCOUNT.USERNAME, USER_ACCOUNT.EMAIL,
                      USER_ACCOUNT.FIRST_NAME, USER_ACCOUNT.LAST_NAME, USER_ACCOUNT.MIDDLE_NAME,
                      USER_ACCOUNT.ORG_UNIT_ID)
                  .values(userAccountId, username, username + "@test.com", username, username, username, organizationID),
              dslContext
                  .insertInto(SIMPLE_CREDENTIALS,
                      SIMPLE_CREDENTIALS.USER_ACCOUNT_ID, SIMPLE_CREDENTIALS.PASSWORD)
                  .values(userAccountId, username))
          .execute();
    }
    catch (SQLException e) {
      throw new RuntimeException("Failed to create user", e);
    }
  }

  public void createGroupBindRoles(String groupName, String accessLevel, String... roles) {
    String groupId = generateId();
    try (Connection connection = securityDataSource.getConnection()) {
      dslContext = DSL.using(connection, settings);
      dslContext.insertInto(USER_GROUP, USER_GROUP.NAME, USER_GROUP.USER_GROUP_ID, USER_GROUP.ACCESS_LEVEL_ID)
          .values(groupName, groupId, accessLevel)
          .execute();

      Arrays.asList(roles).forEach(role -> bindRoleToGroup(role, groupId));
    }
    catch (SQLException e) {
      throw new RuntimeException("Failed to create Group");
    }
  }

  public void createRoleBindGroups(String roleName, String... groupIds) {
    String userRoleId = generateId();
    try (Connection connection = securityDataSource.getConnection()) {
      dslContext = DSL.using(connection, settings);
      dslContext.insertInto(USER_ROLE, USER_ROLE.NAME, USER_ROLE.USER_ROLE_ID)
          .values(roleName, userRoleId)
          .execute();

      Arrays.asList(groupIds).forEach(groupId -> bindRoleToGroup(userRoleId, groupId));
    }
    catch (SQLException e) {
      throw new RuntimeException("Failed to create role");
    }
  }

  public void createUserBindGroups(String userName, String organizationID, String... groupIDs) {
    String userAccountId = generateId();
    try (Connection connection = securityDataSource.getConnection()) {
      dslContext = DSL.using(connection, settings);
      dslContext.batch(
              dslContext.insertInto(USER_ACCOUNT, USER_ACCOUNT.USER_ACCOUNT_ID, USER_ACCOUNT.EMAIL,
                      USER_ACCOUNT.FIRST_NAME,
                      USER_ACCOUNT.LAST_NAME, USER_ACCOUNT.MIDDLE_NAME, USER_ACCOUNT.ORG_UNIT_ID,
                      USER_ACCOUNT.USERNAME)
                  .values(userAccountId, userName + "@test.com", userName, userName, userName,
                      organizationID, userName),
              dslContext.insertInto(SIMPLE_CREDENTIALS, SIMPLE_CREDENTIALS.USER_ACCOUNT_ID,
                      SIMPLE_CREDENTIALS.PASSWORD)
                  .values(userAccountId, userName))
          .execute();

      Arrays.asList(groupIDs).forEach(group -> bindGroupToUser(group, userAccountId));
    }
    catch (SQLException e) {
      throw new RuntimeException("Failed to create user", e);
    }
  }

  private void bindRoleToGroup(String roleId, String groupId) {
    String linkUserGroupUserRoleId = generateId();
    dslContext.insertInto(LINK_USER_GROUP_USER_ROLE,
            LINK_USER_GROUP_USER_ROLE.LINK_USER_GROUP_USER_ROLE_ID,
            LINK_USER_GROUP_USER_ROLE.USER_GROUP_ID, LINK_USER_GROUP_USER_ROLE.USER_ROLE_ID)
        .values(linkUserGroupUserRoleId, groupId, roleId)
        .execute();
  }

  private void bindGroupToUser(String groupId, String userAccountId) {
    String linkUserAccountUserGroupId = generateId();

    dslContext.insertInto(LINK_USER_ACCOUNT_USER_GROUP,
            LINK_USER_ACCOUNT_USER_GROUP.LINK_USER_ACCOUNT_USER_GROUP_ID,
            LINK_USER_ACCOUNT_USER_GROUP.USER_ACCOUNT_ID,
            LINK_USER_ACCOUNT_USER_GROUP.USER_GROUP_ID)
        .values(linkUserAccountUserGroupId, userAccountId, groupId)
        .execute();
  }

  public void deleteLinkedRolesFromGroup(String groupId) {
    try (Connection connection = securityDataSource.getConnection()) {
      dslContext = DSL.using(connection, settings);
      dslContext.delete(LINK_USER_GROUP_USER_ROLE)
          .where(LINK_USER_GROUP_USER_ROLE.USER_ROLE_ID.eq(groupId))
          .execute();
    }
    catch (SQLException e) {
      throw new RuntimeException("Failed to delete linked group roles", e);
    }
  }

  public void deleteLinkedGroupsFromRole(String roleId) {
    try (Connection connection = securityDataSource.getConnection()) {
      dslContext = DSL.using(connection, settings);
      dslContext.delete(LINK_USER_GROUP_USER_ROLE)
          .where(LINK_USER_GROUP_USER_ROLE.USER_ROLE_ID.eq(roleId))
          .execute();
    }
    catch (SQLException e) {
      throw new RuntimeException("Failed to delete linked role groups", e);
    }
  }

  public void deleteUserLinkedGroups(String userAccountID) {
    try (Connection connection = securityDataSource.getConnection()) {
      dslContext = DSL.using(connection, settings);
      dslContext.delete(LINK_USER_ACCOUNT_USER_GROUP)
          .where(LINK_USER_ACCOUNT_USER_GROUP.USER_ACCOUNT_ID.eq(userAccountID))
          .execute();
    }
    catch (SQLException e) {
      throw new RuntimeException("Failed to delete linked user groups", e);
    }
  }

  public void deleteTestGroup(String groupName) {
    deleteSecurityRecord(USER_GROUP, USER_GROUP.NAME, groupName);
  }

  public void createTestGroup(String groupName, String accessLevelId) {
    insertSecurityRecord(USER_GROUP, USER_GROUP.USER_GROUP_ID, USER_GROUP.NAME, USER_GROUP.ACCESS_LEVEL_ID,
        generateId(), groupName, accessLevelId);
  }

  public void createBindGroupRole(String name, String groupId, String accessLevelId, String roleId) {
    try (Connection connection = securityDataSource.getConnection()) {
      dslContext = DSL.using(connection, settings);
      dslContext.batch(
              dslContext.insertInto(USER_GROUP, USER_GROUP.USER_GROUP_ID, USER_GROUP.NAME, USER_GROUP.ACCESS_LEVEL_ID)
                  .values(groupId, name, accessLevelId),
              dslContext.insertInto(USER_ROLE, USER_ROLE.USER_ROLE_ID, USER_ROLE.NAME)
                  .values(roleId, name),
              dslContext.insertInto(LINK_USER_GROUP_USER_ROLE, LINK_USER_GROUP_USER_ROLE.LINK_USER_GROUP_USER_ROLE_ID,
                      LINK_USER_GROUP_USER_ROLE.USER_GROUP_ID, LINK_USER_GROUP_USER_ROLE.USER_ROLE_ID)
                  .values(generateId(), groupId, roleId))
          .execute();
    }
    catch (SQLException e) {
      throw new RuntimeException("Failed to create and bind group with role", e);
    }
  }

  public void deleteUserGroupRole(String name, String groupId, String roleId) {
    try (Connection connection = securityDataSource.getConnection()) {
      dslContext = DSL.using(connection, settings);
      dslContext.batch(
              dslContext.delete(USER_ACCOUNT)
                  .where(USER_ACCOUNT.FIRST_NAME.eq(name)),
              dslContext.delete(USER_GROUP)
                  .where(USER_GROUP.USER_GROUP_ID.eq(groupId)),
              dslContext.delete(USER_ROLE)
                  .where(USER_ROLE.USER_ROLE_ID.eq(roleId)))
          .execute();
    }
    catch (SQLException e) {
      throw new RuntimeException("Failed to delete user, group, role", e);
    }
  }

  public void linkRoleToAuthority(String roleId, String authorityId) {
    insertSecurityRecord(LINK_USER_ROLE_AUTHORITY, LINK_USER_ROLE_AUTHORITY.USER_ROLE_AUTHORITY_ID,
        LINK_USER_ROLE_AUTHORITY.USER_ROLE_ID, LINK_USER_ROLE_AUTHORITY.AUTHORITY_ID,
        generateId(), roleId, authorityId);
  }

  public void deleteTestRole(String roleName) {
    deleteSecurityRecord(USER_ROLE, USER_ROLE.NAME, roleName);
  }

  public void createTestRole(String roleName) {
    try (Connection connection = securityDataSource.getConnection()) {
      dslContext = DSL.using(connection, settings);
      dslContext.insertInto(USER_ROLE, USER_ROLE.USER_ROLE_ID, USER_ROLE.NAME)
          .values(generateId(), roleName)
          .execute();
    }
    catch (SQLException e) {
      throw new RuntimeException("Failed to create role", e);
    }
  }

  public boolean hasUserByData(String username, String email) {
    try (Connection connection = securityDataSource.getConnection()) {
      dslContext = DSL.using(connection, settings);
      int count = dslContext.selectCount()
          .from(USER_ACCOUNT)
          .where(USER_ACCOUNT.EMAIL.eq(email))
          .and(USER_ACCOUNT.FIRST_NAME.eq(username))
          .and(USER_ACCOUNT.LAST_NAME.eq(username))
          .and(USER_ACCOUNT.MIDDLE_NAME.eq(username))
          .execute();

      return count == 1;
    }
    catch (SQLException e) {
      throw new RuntimeException("Failed to fetch user count", e);
    }
  }
}
