package org.micord.service;

import com.arangodb.ArangoCursor;
import com.arangodb.ArangoDBException;
import com.arangodb.ArangoDatabase;
import com.arangodb.model.AqlQueryOptions;
import org.micord.config.ArangoDBConnection;
import org.micord.config.DatabaseConnection;
import org.micord.exceptions.NoDownloadReportRecordsException;
import org.micord.models.SqlDownloadBuildQueryResponse;
import org.micord.models.requests.RequestParameters;
import org.micord.models.requests.downloads.AQLDownloadRequest;
import org.micord.models.requests.downloads.BaseDownloadRequest;
import org.micord.models.requests.downloads.SQLDownloadRequest;
import org.micord.models.requests.RequestArgument;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.LocalDate;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

@Service
public class DownloadService {

	private static final Logger logger = LoggerFactory.getLogger(DownloadService.class);
	private static final String REQUEST_WITH_ADDITIONAL_ID = "requestWithAdditionalId";
	private static final int limit = 600000;

	public File download(BaseDownloadRequest selectedRequest, List<String> ids, RequestParameters parameters, Map<String, Boolean> validationResults) throws SQLException {
		LocalDate startDate = parameters.getStartDate();
		LocalDate endDate = parameters.getEndDate();
		if (selectedRequest instanceof SQLDownloadRequest) {
			return processSqlDownloadRequest((SQLDownloadRequest) selectedRequest, ids, startDate, endDate, validationResults);
		} else if (selectedRequest instanceof AQLDownloadRequest) {
			return processAqlDownloadRequest((AQLDownloadRequest) selectedRequest, ids, startDate, endDate, validationResults);
		}
		throw new IllegalArgumentException("Unsupported request type: " + selectedRequest.getClass().getSimpleName());
	}

	private File processAqlDownloadRequest(AQLDownloadRequest request, List<String> ids, LocalDate startDate, LocalDate endDate, Map<String, Boolean> validationResults) throws SQLException {
		try {
			ArangoDatabase arangoDb = ArangoDBConnection.getConnection(request.getAqlConnectionParams());

			Boolean emptyIdsAllowed = validationResults.get(ValidationService.IS_EMPTY_IDS_ALLOWED);
			Boolean emptyDatesAllowed = validationResults.get(ValidationService.IS_EMPTY_DATES_ALLOWED);

			List<Map<String, Object>> entities = executeSelectAqlRequest(
				arangoDb,
				request.getDownloadRequestEntitySelectorQuery(),
				ids,
				startDate,
				endDate,
				emptyIdsAllowed,
				emptyDatesAllowed
			);

			// решили отдавать пустой файл
//			if (entities.isEmpty()) {
//				logger.warn("No entities found for main AQL request.");
//				throw new NoDownloadReportRecordsException(
//					"Отчет не может быть сгенерирован. Нет записей в базе для успешной генерации."
//				);
//			}

			int arangoLimit = limit;
			if (request.getAqlDownloadLimit() != null) {
				arangoLimit = request.getAqlDownloadLimit();
			}

			return writeResultsToCsv(entities, arangoLimit);

		} catch (ArangoDBException e) {
			logger.error("Error connecting to ArangoDB or executing AQL query: {}", e.getMessage(), e);
			throw new SQLException("Ошибка работы с базой данных. Попробуйте позже.", e);
		} catch (NoDownloadReportRecordsException e) {
			logger.warn("No records available for report generation: {}", e.getMessage());
			throw e;
		} catch (Exception e) {
			logger.error("Unexpected error occurred during report generation: {}", e.getMessage(), e);
			throw new RuntimeException("Произошла непредвиденная ошибка при генерации отчета.", e);
		}

//		request.getAqlRequestCollections().forEach(collection -> {
//			String type = collection.getCollectionUrl();
//			String entityType;
//
//			if (Objects.equals(type, "applications")) {
//				entityType = "applicationId";
//			} else {
//				entityType = type + "Id";
//			}
//
//			Object entityIds = entities.get(entityType);
//
//			if (entityIds instanceof String) {
//				entityIds = Collections.singletonList((String) entityIds);
//			}
//
//			String aqlQuery = buildAqlQuery(type, ids, collection.getDateAttribute(), startDate, endDate, emptyIdsAllowed, emptyDatesAllowed);
//
//			results.addAll(executeAqlQuery(arangoDb, aqlQuery, (List<String>) entityIds, startDate, endDate, emptyIdsAllowed, emptyDatesAllowed));
//		});

//		return writeResultsToCsv(results);
	}

	private File processSqlDownloadRequest(SQLDownloadRequest request,
			List<String> ids, LocalDate startDate, LocalDate endDate,
			Map<String, Boolean> validationResults) {
		List<File> allFiles = new ArrayList<>();
		int offset = 0;
		int pageIndex = 1;
		int postgresLimit = limit;
		if (request.getRequestArgumentLimit() != null) {
			postgresLimit = request.getRequestArgumentLimit();
		}
		List<String[]> results = new ArrayList<>();
		try (Connection connection = DatabaseConnection.getConnection(
				request.getSqlConnectionParams())) {
			Map<String, Object> query = buildSqlQuery(request, ids, startDate, endDate,
					validationResults, postgresLimit, offset
			);
			if (query.get(REQUEST_WITH_ADDITIONAL_ID) != null) {
				pageIndex--;
				SqlDownloadBuildQueryResponse response = (SqlDownloadBuildQueryResponse) query.get(REQUEST_WITH_ADDITIONAL_ID);
				String url = response.getSql();
				String paramName = response.getParamName();
				Collection<List<String>> pages = partitionList(response.getParams(), postgresLimit);
				url = url.replaceAll(";(?=[^;]*$)", " ") + " LIMIT " + postgresLimit + " OFFSET "
							+ offset + ";";
				for (List<String> page : pages) {
					pageIndex++;
					String resultSet = "(" + page.stream()
							.map(s -> "'" + s.trim() + "'")
							.collect(Collectors.joining(", ")) + ")";
					url = url.replace("${" + paramName + "}",
							resultSet
					);
					String newUrl = url;
					int newOffset = offset;
					while (true) {
						results = new ArrayList<>(executeSqlQuery(connection, newUrl));
						if (results.size() <= 1) {
							pageIndex--;
							break;
						}
						File file = writeSingleSqlCsvFile(results, "download-part" + pageIndex + "-", ".csv");
						allFiles.add(file);
						if (results.size() - 1 < postgresLimit) {
							break;
						}
						String oldPagination = " OFFSET " + newOffset + ";";
						newOffset += postgresLimit;
						String newPagination = " OFFSET " + newOffset + ";";
						newUrl = newUrl.replace(oldPagination, newPagination);
						pageIndex++;
					}
				}
			}
			else {
				String requestURL = (String) query.get("requestURL");
				while (true) {
					results = new ArrayList<>(executeSqlQuery(connection, requestURL));
					if (results.size() <= 1) {
						break;
					}
					File file = writeSingleSqlCsvFile(results, "download-part" + pageIndex + "-", ".csv");
					allFiles.add(file);
					if (results.size() - 1 < postgresLimit) {
						break;
					}
					String oldPagination = " OFFSET " + offset + ";";
					offset += postgresLimit;
					String newPagination = " OFFSET " + offset + ";";
					requestURL = requestURL.replace(oldPagination, newPagination);
					pageIndex++;

				}
			}
			if (allFiles.isEmpty()) {
				return writeSingleSqlCsvFile(results, "download-part 0", ".csv");
			}
			if (allFiles.size() == 1) {
				return allFiles.get(0);
			}
			else {
				return createZipArchive(allFiles, "download-");
			}
		}
		catch (SQLException | IOException e) {
			throw new RuntimeException("SQL execution failed for query " + request.getRequestURL()
					.replaceAll("password=\\S+", "password=***"), e);
		}
	}

	private File writeSingleSqlCsvFile(List<String[]> results, String prefix, String suffix) throws IOException {
		File csvFile = File.createTempFile(prefix, suffix);

		try (PrintWriter writer = new PrintWriter(
			new OutputStreamWriter(new FileOutputStream(csvFile), StandardCharsets.UTF_8))) {
			String lineSeparator = "\r\n";

			for (String[] row : results) {
				writer.print(formatCsvRow(row));
				writer.print(lineSeparator);
			}
		}

		return csvFile;
	}

	private List<Map<String, Object>> executeSelectAqlRequest(ArangoDatabase arangoDb,
														String downloadRequestEntitySelectorQuery,
														List<String> ids, LocalDate startDate, LocalDate endDate, Boolean emptyIdsAllowed, Boolean emptyDatesAllowed) {
		List<Map<String, Object>> results = new ArrayList<>();


		try {
			Map<String, Object> bindVars = new HashMap<>();
			if (!emptyIdsAllowed && ids != null && !ids.isEmpty()) {
				bindVars.put("ids", ids);
			}
			if (!emptyDatesAllowed) {
				if (startDate != null) {
					bindVars.put("startDate", startDate.toString());
				}
				if (endDate != null) {
					bindVars.put("endDate", endDate.toString());
				}
			}

			logger.info("Executing AQL query: {}\nWith bindVars: {}", downloadRequestEntitySelectorQuery, bindVars);

			AqlQueryOptions aqlQueryOptions = new AqlQueryOptions();
			try (ArangoCursor<Map> cursor = arangoDb.query(downloadRequestEntitySelectorQuery, Map.class, bindVars, aqlQueryOptions)) {
				while (cursor.hasNext()) {
					results.add(cursor.next());
				}
			} catch (IOException e) {
				throw new RuntimeException(e);
			}

		} catch (ArangoDBException e) {
			logger.error("AQL query execution failed: {}\nError: {}", downloadRequestEntitySelectorQuery, e.getMessage(), e);
		}
		return results;
//		Map<String, Object> entities = new HashMap<>();
//
//		Map<String, Object> bindVars = new HashMap<>();
//		if (!emptyIdsAllowed && ids != null && !ids.isEmpty()) {
//			bindVars.put("ids", ids);
//		}
//		if (!emptyDatesAllowed) {
//			if (startDate != null) {
//				bindVars.put("startDate", startDate.toString());
//			}
//			if (endDate != null) {
//				bindVars.put("endDate", endDate.toString());
//			}
//		}
//
//		logger.info("Executing AQL query: {}\nWith bindVars: {}", aqlQuery, bindVars);
//
//		AqlQueryOptions aqlQueryOptions = new AqlQueryOptions();
//
//		try (ArangoCursor<Map> cursor = arangoDb.query(downloadRequestEntitySelectorQuery, Map.class, bindVars, aqlQueryOptions)) {
//			while (cursor.hasNext()) {
//				Map<String, Object> result = cursor.next();
//
//				for (Map.Entry<String, Object> entry : result.entrySet()) {
//					String key = entry.getKey();
//					Object entityValue = entry.getValue();
//
//					entities.put(key, entityValue);
//				}
//			}
//		}
//		catch (Exception e) {
//			logger.error("Failed to execute AQL url", e);
//		}
//
//		return entities;
	}

	private String buildAqlQuery(String collectionName, List<String> ids, String dateAttribute, LocalDate startDate, LocalDate endDate, Boolean emptyIdsAllowed, Boolean emptyDatesAllowed) {
		StringBuilder queryBuilder = new StringBuilder();
		queryBuilder.append("FOR doc IN ").append(collectionName).append(" ");

		List<String> conditions = new ArrayList<>();
		if (!emptyIdsAllowed && ids != null && !ids.isEmpty()) {
			conditions.add("doc._key IN @ids");
		}
		if (!emptyDatesAllowed && dateAttribute != null) {
			if (startDate != null) {
				conditions.add("doc." + dateAttribute + " >= @startDate");
			}
			if (endDate != null) {
				conditions.add("doc." + dateAttribute + " <= @endDate");
			}
		}

		if (!conditions.isEmpty()) {
			queryBuilder.append("FILTER ").append(String.join(" AND ", conditions)).append(" ");
		}

		queryBuilder.append("RETURN doc");

		return queryBuilder.toString();
	}

	private List<Map<String, Object>> executeAqlQuery(ArangoDatabase arangoDb, String aqlQuery, List<String> ids, LocalDate startDate, LocalDate endDate, Boolean emptyIdsAllowed, Boolean emptyDatesAllowed) {
		List<Map<String, Object>> results = new ArrayList<>();


		try {
			Map<String, Object> bindVars = new HashMap<>();
			if (!emptyIdsAllowed && ids != null && !ids.isEmpty()) {
				bindVars.put("ids", ids);
			}
			if (!emptyDatesAllowed) {
				if (startDate != null) {
					bindVars.put("startDate", startDate.toString());
				}
				if (endDate != null) {
					bindVars.put("endDate", endDate.toString());
				}
			}

			logger.info("Executing AQL query: {}\nWith bindVars: {}", aqlQuery, bindVars);

			AqlQueryOptions aqlQueryOptions = new AqlQueryOptions();
			try (ArangoCursor<Map> cursor = arangoDb.query(aqlQuery, Map.class, bindVars, aqlQueryOptions)) {
				while (cursor.hasNext()) {
					results.add(cursor.next());
				}
			} catch (IOException e) {
				throw new RuntimeException(e);
			}

		} catch (ArangoDBException e) {
			logger.error("AQL query execution failed: {}\nError: {}", aqlQuery, e.getMessage(), e);
		}
		return results;
	}

	private File writeResultsToCsv(List<Map<String, Object>> results, int limit) {
		try {
			// If results fit in a single file, create one CSV
			if (results.size() <= limit) {
				return writeSingleCsvFile(results, "arango-download-", ".csv");
			}

			// Otherwise, create multiple CSV files and zip them
			List<File> csvFiles = new ArrayList<>();
			int fileIndex = 1;

			for (int i = 0; i < results.size(); i += limit) {
				int endIndex = Math.min(i + limit, results.size());
				List<Map<String, Object>> chunk = results.subList(i, endIndex);

				File csvFile = writeSingleCsvFile(chunk, "arango-download-part" + fileIndex + "-", ".csv");
				if (csvFile != null) {
					csvFiles.add(csvFile);
					fileIndex++;
				}
			}

			// Create ZIP archive
			return createZipArchive(csvFiles, "arango-download-");

		} catch (IOException e) {
			logger.error("Failed to write results to CSV", e);
			return null;
		}
	}

	private File writeSingleCsvFile(List<Map<String, Object>> results, String prefix, String suffix) throws IOException {
		File csvFile = File.createTempFile(prefix, suffix);

		try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(csvFile), StandardCharsets.UTF_8))) {
			if (!results.isEmpty()) {
				List<String> headers = new ArrayList<>(results.get(0).keySet());
				writer.write(String.join(",", headers));
				writer.newLine();

				for (Map<String, Object> row : results) {
					List<String> rowValues = headers.stream()
						.map(header -> formatCsvField(row.get(header)))
						.collect(Collectors.toList());
					writer.write(String.join(",", rowValues));
					writer.newLine();
				}
			}
		}
		return csvFile;
	}

	private File createZipArchive(List<File> files, String prefix) throws IOException {
		File zipFile = File.createTempFile(prefix, ".zip");

		try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) {
			for (File file : files) {
				addFileToZip(file, zos);
				// Delete temporary CSV files after adding to ZIP
				if (!file.delete()) {
					logger.warn("Failed to delete temporary file: {}", file.getName());
				}
			}
		}

		return zipFile;
	}

	private void addFileToZip(File file, ZipOutputStream zos) throws IOException {
		try (FileInputStream fis = new FileInputStream(file)) {
			ZipEntry zipEntry = new ZipEntry(file.getName());
			zos.putNextEntry(zipEntry);

			byte[] buffer = new byte[8192];
			int length;
			while ((length = fis.read(buffer)) >= 0) {
				zos.write(buffer, 0, length);
			}

			zos.closeEntry();
		}
	}

	private String formatCsvField(Object value) {
		if (value == null) {
			return "\"\"";
		}
		String strValue = value.toString().replace("\"", "\"\"");
		return "\"" + strValue + "\"";
	}

	private String formatCsvRow(String[] row) {
		StringBuilder formattedRow = new StringBuilder();

		for (int i = 0; i < row.length; i++) {
			if (i > 0) {
				formattedRow.append(",");
			}

			formattedRow.append("\"");
			formattedRow.append(escapeCsvField(row[i]));
			formattedRow.append("\"");
		}

		return formattedRow.toString();
	}

	private String escapeCsvField(String field) {
		if (field == null) {
			return "";
		}
		return field.replace("\"", "\"\"");
	}

	private Map<String, Object> buildSqlQuery(SQLDownloadRequest request, List<String> ids,
			LocalDate startDate, LocalDate endDate, Map<String, Boolean> validationResults, int limit,
			int offset) {
		Boolean emptyIdsAllowed = validationResults.get(ValidationService.IS_EMPTY_IDS_ALLOWED);
		Boolean emptyDatesAllowed = validationResults.get(ValidationService.IS_EMPTY_DATES_ALLOWED);

		Map<String, Object> resultMap = new HashMap<>();
		String endpointArguments = "";

		String requestURL = request.getRequestURL();

		if (!emptyDatesAllowed) {
			requestURL = prepareRequestURL(request.getRequestURL(), startDate, endDate);
			requestURL = requestURL.replace("${DB}", request.getSqlConnectionParams().getJdbcDatabase());
		}

		if (emptyIdsAllowed != null && emptyIdsAllowed) {
			requestURL = requestURL.replace("where id in ${endpointArguments}", "");
		}
		else if (ids == null || ids.isEmpty()) {
			requestURL = requestURL.replace("where id in ${endpointArguments}", "");
		}
		else {
			if (requestURL.contains(":=")) {
				endpointArguments =
						"'{" + ids.stream().map(String::trim).collect(Collectors.joining(", ")) + "}'";
			}
			else {
				endpointArguments =
						"(" + ids.stream().map(s -> "'" + s.trim() + "'").collect(Collectors.joining(", "))
						+ ")";
			}
			requestURL = requestURL.replace("${endpointArguments}", endpointArguments);
		}
		Map<String, List<String>> params = new HashMap<>();
		if (request.getRequestArguments() != null && !request.getRequestArguments().isEmpty()) {
			for (RequestArgument argument : request.getRequestArguments()) {
				if (argument.getRequestArgumentConnectionParams() != null) {
					try (Connection connection = DatabaseConnection.getConnection(
							argument.getRequestArgumentConnectionParams())) {
						String query = argument.getRequestArgumentURL();
						query = prepareRequestURL(query, startDate, endDate);
						query = query.replace("${endpointArguments}", endpointArguments);
						int subOffset = 0;
						List<String> aggregatedIds = new ArrayList<>();
						while (true) {
							String paginatedQuery = query.replaceAll(";(?=[^;]*$)", " ") + " LIMIT " + limit + " OFFSET " + subOffset;
							List<String> result = fetchFileListFromDatabaseSQL(connection, paginatedQuery);
							if (result.isEmpty()) break;
							aggregatedIds.addAll(result);
							if (result.size() < limit) break;
							subOffset += limit;
						}
						params.put(argument.getRequestArgumentName(), aggregatedIds);
					}
					catch (SQLException e) {
						logger.error("Failed to execute query for RequestArgument", e);
					}
				}
			}
		}
		if (!params.isEmpty()) {
			if (params.size() == 1) {
				Map.Entry<String, List<String>> entry = params.entrySet().iterator().next();
				String key = entry.getKey();
				List<String> value = entry.getValue();
				resultMap.put(REQUEST_WITH_ADDITIONAL_ID, new SqlDownloadBuildQueryResponse(requestURL, key, value));
				return resultMap;
			}
			else {
				for (Map.Entry<String, List<String>> entry : params.entrySet()) {
					String resultSet = "(" + entry.getValue().stream()
							.map(s -> "'" + s.trim() + "'")
							.collect(Collectors.joining(", ")) + ")";
					requestURL = requestURL.replace("${" + entry.getKey() + "}",
							resultSet
					);
				}
				requestURL = requestURL.replaceAll(";(?=[^;]*$)", " ") + " LIMIT " + limit + " OFFSET "
										 + offset + ";";
			}
		}
		else {
			requestURL = requestURL.replaceAll(";(?=[^;]*$)", " ") + " LIMIT " + limit + " OFFSET "
									 + offset + ";";
		}

		resultMap.put("requestURL", requestURL);
		return resultMap;
	}

	private String prepareRequestURL(String requestURL, LocalDate startDate,
									 LocalDate endDate) {
		if (startDate != null) {
			requestURL = requestURL.replace("${startDate}", startDate.toString());
		}
		if (endDate != null) {
			requestURL = requestURL.replace("${endDate}", endDate.toString());
		}
		return requestURL;
	}

	private List<String[]> executeSqlQuery(Connection connection, String query) throws SQLException {
		List<String[]> results = new ArrayList<>();
		try (PreparedStatement stmt = connection.prepareStatement(query);
			 ResultSet resultSet = stmt.executeQuery()) {

			int columnCount = resultSet.getMetaData().getColumnCount();
			// Add headers
			String[] headers = new String[columnCount];
			for (int i = 1; i <= columnCount; i++) {
				headers[i - 1] = resultSet.getMetaData().getColumnName(i);
			}
			results.add(headers);

			// Add rows
			while (resultSet.next()) {
				String[] row = new String[columnCount];
				for (int i = 1; i <= columnCount; i++) {
					row[i - 1] = resultSet.getString(i);
				}
				results.add(row);
			}
		}
		return results;
	}

	public List<String> fetchFileListFromDatabaseSQL(Connection connection, String query)
		throws SQLException {
		List<String> results = new ArrayList<>();
		try (PreparedStatement stmt = connection.prepareStatement(query);
			 ResultSet rs = stmt.executeQuery()) {
			while (rs.next()) {
				results.add(rs.getString(1)); // Fetch the first column
			}
		}
		return results;
	}

	private <T> Collection<List<T>> partitionList(List<T> list, int size) {
		return IntStream.range(0, (list.size() + size - 1) / size)
				.mapToObj(i -> list.subList(i * size, Math.min((i + 1) * size, list.size())))
				.map(ArrayList::new)
				.collect(Collectors.toList());
	}
}
