package com.mycompany.moduloseguridad.utilities;

import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Types;
import org.json.JSONArray;
import org.json.JSONObject;

public final class DAOHelper {

	//********************************************** STATEMENT **************************************************************
	public static JSONArray queryPS(Connection connection, String query, JSONArray... params) throws Exception {
		return queryPS(connection, query, false, params);
	}
        
	public static JSONArray queryPS(Connection connection, String query, boolean mode, JSONArray... params) throws Exception {
		JSONArray list = new JSONArray();
		try (PreparedStatement ps = connection.prepareStatement(query);){
			
			if (params != null && params.length > 0) {
				JSONArray _params = params[0];
				int index = 1;
				//Recorremos la lista de parametros y lo seteamos en el preparedstatement
				for (Object p : _params) {
					setPreparedStatement(ps, index++, p);
				}
			}
			System.out.println("Query ejecutado con queryPS ---->>> " + ps);
			ResultSet rs = ps.executeQuery();
			ResultSetMetaData rm = rs.getMetaData();
			int numCols = rm.getColumnCount();
			while (rs.next()) {
				JSONObject obj = new JSONObject();
				for (int i = 1; i <= numCols; i++) {
					setJSONObject(rs, rm, i, obj, mode);
				}
				list.put(obj);
			}
		} catch (Exception ex) {
          
			ex.printStackTrace();
            System.out.println(""+ex.getMessage());
			throw ex;
		}
		return list;
	}

	public static JSONObject executePS(Connection connection, String query, JSONArray... params) {
		JSONObject obj = new JSONObject();
		try (PreparedStatement ps = connection.prepareStatement(query);){
			
			if (params != null && params.length > 0) {
				JSONArray _params = params[0];
				int index = 1;
				for (Object parametro : _params) {
					setPreparedStatement(ps, index++, parametro);
				}
			}
			int filas = ps.executeUpdate();
			obj.put("status", filas > 0);
			obj.put("message", "OK");
			obj.put("data", filas);
		} catch (Exception ex) {
                    ex.printStackTrace();
			obj.put("status", false);
			obj.put("message", ex);
			obj.put("data", JSONObject.NULL);
		}
		return obj;
	}

//*************************************** PROCEDIMIENTOS ALMACENADOS ****************************************************
	public static JSONArray queryProcedure(Connection cn, String query, JSONArray... parameters) throws Exception {
		return queryProcedure(cn, query, false, parameters);
	}

	public static JSONArray queryProcedure(Connection cn, String query, boolean mode, JSONArray... parameters) throws Exception {
		JSONArray result = new JSONArray();
		JSONObject outputParamTypes = new JSONObject();
		JSONArray params = null;
		JSONArray outputParams = new JSONArray();
		try (CallableStatement cs = cn.prepareCall(query);){
			if (parameters != null && parameters.length > 0) {
				params = parameters[0];
				int index = 1;
				for (Object parameter : params) {
					if (parameter instanceof Class) {
						outputParams.put(index);
						registerOutputParameter(cs, index++, parameter, outputParamTypes);
					} else {
						setPreparedStatement(cs, index++, parameter);
					}
				}
			}
			boolean isResultSet = cs.execute();
			System.out.println("CONSULTA\n" + cs.toString());
			if (isResultSet) {
				try(final ResultSet rs = cs.getResultSet()){                                  
					ResultSetMetaData rm = rs.getMetaData();
					int columnCount = rm.getColumnCount();
					while (rs.next()) {
						JSONObject obj = new JSONObject();
						for (int columnIndex = 1; columnIndex <= columnCount; columnIndex++) {
							setJSONObject(rs, rm, columnIndex, obj, mode);
						}
						result.put(obj);                                              
					}
				}
			} else {
				int rowsAffected = cs.getUpdateCount();
				JSONObject obj = new JSONObject();
				obj.put("rowsAffected", rowsAffected);
				result.put(obj);
			}

			if (!outputParamTypes.isEmpty()) {
				clearJSONArray(params);
			}
			JSONArray finalParams = params;
			outputParams.forEach((index) -> {
				int type = outputParamTypes.getInt(String.valueOf(index));
				try {
					getOutputParameter(cs, (Integer) index, type, finalParams);
				} catch (SQLException e) {
					throw new RuntimeException(e);
				}
			});

		} catch (Exception ex) {
            System.out.println(ex.getMessage());
		}
		return result;
	}

	public static JSONObject queryWithPaging(Connection cn, String query, JSONObject data) throws Exception {
		/*
		- En la descripción se hace referencia a queryWithPaging como MÉTODO y al procedimiento almacenado o función de base de datos como FUNCIÓN.
		El método sirve para obtener los datos necesarios para un datatable y permite también listar todos los registros
		de una función que recibe limit y offset como parámetros (paginado)
		- En la función los últimos parámetros deben ser LIMIT y OFFSET en ese orden.
		- La consulta debe retornar una columna con el total de registros y esta debe llamarse "records_total".
		- Este método trabaja con length y start que son parámetros que envía el datatable para realizar el paginado(serverSide);
			ambos son asignados(pst.set()) a la función en ese mismo orden(primero LENGTH para LIMIT y luego START para OFFSET).
		- Si no se manda length ni start se asigna null a ambos parámetros de la función y se listan todos los registros disponibles.
		- El método retorna lo siguiente:
			- recordsTotal = "records_total" de la consulta
			- recordsFiltered = "records_total" de la consulta
			- draw = recibido del mismo datatable
			- data = de tipo JSONArray y es la lista de datos recibida de la función
		- EJEMPLO:
			String sql = "SELECT * FROM \"general\".func_listar_solicitudes_descuento_mora(?,?,?)";
			return PostgreSqlDAOFactory.queryWithPaging(DatabaseConfig.SIIAA, sql, datos);
			Donde se envían y reciben los valores mencionados anteriormente.
		 */
		JSONObject salida = new JSONObject();
		JSONArray jsonArray = new JSONArray();//objeto que almacena todas las filas obtenidas por el query
		int recordsTotal = 0;
		try {

			try (PreparedStatement ps = cn.prepareStatement(query)) {
				int paramsCount = countChar(query, '?'); // obtenido de Utils.StringUtils
				JSONArray _params = data.has("params") ? data.getJSONArray("params") : new JSONArray();
				if (data.has("length") && data.has("start")) {
					_params
						.put(paramsCount - 2, data.getInt("length"))
						.put(paramsCount - 1, data.getInt("start"));
				}

				if (data.has("draw")) {
					salida.put("draw", data.getInt("draw"));
				}

				for (int i = 0; i < paramsCount; i++) {
					if (!_params.isNull(i)) {
						setPreparedStatement(ps, i + 1, _params.get(i));
					} else {
						ps.setNull(i + 1, java.sql.Types.NULL); // Modificado 08/02/2020 JCYL, el conector de MySql actúa diferente al de PostgreSql, no permite setear null con el método general
//						setPreparedStatement(ps, i + 1, JSONObject.NULL);
					}
				}

				System.out.println("Query ejecutado con queryWithPaging ---->>> \n" + ps);
				try (ResultSet rs = ps.executeQuery()) {
					ResultSetMetaData rm = rs.getMetaData();
					int numCols = rm.getColumnCount();
					while (rs.next()) {
						JSONObject obj = new JSONObject();
						for (int i = 1; i <= numCols; i++) {
							setJSONObject(rs, rm, i, obj, false);
						}
						if (obj.has("records_total") && !obj.isNull("records_total")) {
							if (recordsTotal == 0) {
								recordsTotal = Integer.parseInt(String.valueOf(obj.get("records_total")));
							}
							obj.remove("records_total");
						}
						jsonArray.put(obj);
					}
				}
			}
			salida.put("status", true);
			salida.put("message", "Consulta realizada correctamente");
		} catch (Exception ex) {
			salida.put("status", false);
			salida.put("message", "Hubo un error al realizar la consulta");
			throw ex;
		} finally {
			try {
				if (cn != null) {
					cn.close();
				}
			} catch (SQLException e) {
				System.out.println("Error al liberar memoria " + e.getMessage());
			}
		}
		salida.put("recordsTotal", recordsTotal);
		salida.put("recordsFiltered", recordsTotal);
		salida.put("data", jsonArray);
		return salida;
	}

	public static JSONObject queryPSSingle(Connection cn, String query, JSONArray... params) throws Exception {
		JSONObject json = new JSONObject();//objeto que almacena todas las filas obtenidas por el query
		boolean status = false;
		//boolean status = false;
		//String message = "";
		try (PreparedStatement ps = cn.prepareStatement(query);) {
			int paramsCount = countChar(query, '?');
			// comentado el 16/08/2019
//			//validamos si existen parametros
//			if (params != null && params.length > 0) {
//				JSONArray _params = params[0];
//				int index = 1;
//				//Recorremos la lista de parametros y lo seteamos en el preparedstatement
//				for (Object p : _params) {
//					setPreparedStatement(ps, index++, p);
//				}
//			}
			for (int i = 0; i < paramsCount; i++) {
				if (!params[0].isNull(i)) {
					setPreparedStatement(ps, i + 1, params[0].get(i));
				} else {
					setPreparedStatement(ps, i + 1, JSONObject.NULL);
				}
			}

			System.out.println("Query ejecutado con queryPSSingle-->>>> \n" + ps);
			try (ResultSet rs = ps.executeQuery();) {
				ResultSetMetaData rm = rs.getMetaData();
				int numCols = rm.getColumnCount();
				if (rs.next()) {
					for (int i = 1; i <= numCols; i++) {
						setJSONObject(rs, rm, i, json, false);
					}
					try {
						json.put("status", json.getBoolean("status"));
					} catch (Exception e) {
						System.out.println("El tipo de dato recibido en 'status' no es de tipo boolean");
						json.put("status", json.getInt("status") == 1);
					}
//					if (json.has("status")) {  // Al trabajar con mysql, donde no existe el tipo de dato booleano, se añade esta condición
//						if (json.get("status") instanceof Number) {
//
//						} else {
//						}
//					} else {
//						json.put("status", true);
//					}
//					json.put("status", json.has("status") ? json.getBoolean("status") : true);
					json.put("message", json.has("message") ? json.getString("message") : "Operación realizada correctamente.");
				} else {
					json.put("status", false).put("message", "Hubo un error al realizar la operación.");
				}
			}
		} catch (Exception ex) {
			System.out.println("Hubo un error al realizar la consulat con queryPSSingle --->> \n" + ex.getMessage());
			throw ex;
		} finally {
			try {
				if (cn != null) {
					cn.close();
				}
			} catch (SQLException e) {
				System.out.println("Error al liberar memoria " + e.getMessage());
			}
		}
		return json;
	}

	//************************************************************************************************************************
	public static JSONObject executeCS(Connection connection, String query, JSONArray... params) {
		JSONObject obj = new JSONObject();
		try (CallableStatement cs = connection.prepareCall(query);){
			
			if (params != null && params.length > 0) {
				JSONArray _params = params[0];
				int index = 1;
				for (Object p : _params) {
					setPreparedStatement(cs, index++, p);
				}
			}
			int filas = cs.executeUpdate();
			obj.put("status", true);
			obj.put("message", "OK");
			obj.put("data", filas);
		} catch (Exception ex) {
			obj.put("status", false);
			obj.put("message", ex);
			obj.put("data", JSONObject.NULL);
		}
		return obj;
	}
	
	private static void setJSONObject(ResultSet rs, ResultSetMetaData rm, int i, JSONObject obj, boolean mode) throws SQLException {
		int type = rm.getColumnType(i);
		switch (type) {
			case Types.VARCHAR:
				obj.put(mode ? "" + i : rm.getColumnLabel(i), rs.getString(i) == null ? JSONObject.NULL : rs.getString(i));
				break;
			case Types.CHAR:
				obj.put(mode ? "" + i : rm.getColumnLabel(i), rs.getString(i) == null ? JSONObject.NULL : rs.getString(i));
				break;
			case Types.INTEGER:
				obj.put(mode ? "" + i : rm.getColumnLabel(i), rs.getInt(i));
				break;
			case Types.BIT:
				obj.put(mode ? "" + i : rm.getColumnLabel(i), rs.getBoolean(i));
				break;
			case Types.BINARY:
				obj.put(mode ? "" + i : rm.getColumnLabel(i), rs.getBytes(i));
				break;
			default:
				obj.put(mode ? "" + i : rm.getColumnLabel(i), rs.getString(i) == null ? JSONObject.NULL : rs.getString(i));
		}
	}

	private static void getOutputParameter(CallableStatement cs, int cont, int tipo, JSONArray parameter) throws SQLException {
		switch (tipo) {
			case Types.INTEGER:
			case Types.NUMERIC:
				parameter.put(cs.getInt(cont));
				break;
			case Types.BOOLEAN:
				parameter.put(cs.getBoolean(cont));
				break;
			case Types.DOUBLE:
				parameter.put(cs.getDouble(cont));
				break;
			case Types.BINARY:
				parameter.put(cs.getBytes(cont));
				break;
			default:
				parameter.put(cs.getString(cont));
		}
	}

	private static void registerOutputParameter(CallableStatement cs, int index, Object p, JSONObject types) throws SQLException {
		if (p.equals(Integer.class)) {
			cs.registerOutParameter(index, Types.INTEGER);
			types.put(Integer.toString(index), Types.INTEGER);
		} else if (p.equals(String.class)) {
			cs.registerOutParameter(index, Types.VARCHAR);
			types.put(Integer.toString(index), Types.VARCHAR);
		} else if (p.equals(Boolean.class)) {
			cs.registerOutParameter(index, Types.BOOLEAN);
			types.put(Integer.toString(index), Types.BOOLEAN);
		} else if (p.equals(Double.class)) {
			cs.registerOutParameter(index, Types.DOUBLE);
			types.put(Integer.toString(index), Types.DOUBLE);
		}
	}

	private static void setPreparedStatement(PreparedStatement ps, int index, Object p) throws SQLException {
		if (p instanceof Integer) {
			ps.setInt(index, (int) p);
		} else if (p instanceof String) {
			ps.setString(index, p.toString());
		} else if (p instanceof Double) {
			ps.setDouble(index, (double) p);
		} else if (p instanceof Boolean) {
			ps.setBoolean(index, (boolean) p);
		} else if (p instanceof byte[]) {
			ps.setBytes(index, (byte[]) p);
		} else if (p instanceof java.sql.Date) {
			ps.setDate(index,(java.sql.Date) p);
		}
	}

	private static void clearJSONArray(JSONArray jsonArray) {
		while (jsonArray != null && jsonArray.length() > 0) {
			jsonArray.remove(0);
		}
	}

	//countChar, método para encontrar cuántas veces se repite un caracter en una cadena,
	//Actualmente empleado para contar cuántos nulos se deben insertar en la llamada a una función (QueryWithPaging DAO)
	public static int countChar(String str, char c) {
		int count = 0;
		for (int i = 0; i < str.length(); i++) {
			if (str.charAt(i) == c) {
				count++;
			}
		}
		return count;
	}
}