package com.realnet.DataLake.Services;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.realnet.DataLake.Entity.BatchData;
import com.realnet.DataLake.Entity.Data_lake;
import com.realnet.DataLake.Repository.BatchDataRepository;
import com.realnet.DataLake.Repository.Data_lakeRepository;
import com.realnet.SureConnect.Service.SureService;
import com.realnet.realm.Entity.Realm;
import com.realnet.realm.Services.RealmService;
import com.realnet.users.entity1.AppUser;
import com.realnet.users.service1.AppUserServiceImpl;
import com.realnet.utils.Port_Constant;

@Service
public class Data_lakeService {
	@Autowired
	private Data_lakeRepository Repository;

	@Autowired
	private BatchDataRepository batchRepo;
	@Autowired
	private AppUserServiceImpl userService;
	@Autowired
	private RealmService realmService;

	@Autowired
	private SureService sureService;

	public Data_lake Savedata(Data_lake data) {

		data.setUpdatedBy(getUser().getUserId());
		data.setCreatedBy(getUser().getUserId());
		data.setAccountId(getUser().getAccount().getAccount_id());

		if (data.getSure_connect_id() != null) {

			data.setSureconnect_name(sureService.getbyid(data.getSure_connect_id()).getConnection_name());
		}

		Data_lake save = Repository.save(data);
		return save;
	}

//	get all with pagination
	public Page<Data_lake> getAllWithPagination(Pageable page) {
		return Repository.findAll(getUser().getUserId(), page);
	}

	public List<Data_lake> getdetails() {
		List<Realm> realm = realmService.findByUserId(getUser().getUserId());
		List<Data_lake> all = Repository.findAll(getUser().getUserId());

		return all;
	}

	public Data_lake getdetailsbyId(Integer id) {
		return Repository.findById(id).get();
	}

	public void delete_by_id(Integer id) {
		Repository.deleteById(id);
	}

	public Data_lake update(Data_lake data, Integer id) {
		Data_lake old = Repository.findById(id).get();
		// id auto-generated hai → update nahi karenge

		if (data.getName() != null) {
			old.setName(data.getName());
		}

		if (data.getUrl() != null) {
			old.setUrl(data.getUrl());
		}

		if (data.getSchedule() != null) {
			old.setSchedule(data.getSchedule());
		}

		if (data.getCron_job() != null) {
			old.setCron_job(data.getCron_job());
		}

		if (data.getJson() != null) {
			old.setJson(data.getJson());
		}

		if (data.getBatch_volume() != null) {
			old.setBatch_volume(data.getBatch_volume());
		}

		if (data.getSure_connect_id() != null) {
			old.setSure_connect_id(data.getSure_connect_id());

			old.setSureconnect_name(sureService.getbyid(data.getSure_connect_id()).getConnection_name());

		}

		if (data.getUrl_endpoint() != null) {
			old.setUrl_endpoint(data.getUrl_endpoint());
		}

		if (data.getCalculated_field_json() != null) {
			old.setCalculated_field_json(data.getCalculated_field_json());
		}

		if (data.getIscalculatedfield() != null) {
			old.setIscalculatedfield(data.getIscalculatedfield());
		}

		if (data.getGroupby_json() != null) {
			old.setGroupby_json(data.getGroupby_json());

		}

		if (data.getRef_datalake_id() != null) {
			old.setRef_datalake_id(data.getRef_datalake_id());

		}

		final Data_lake test = Repository.save(old);

		return test;
	}

	public Data_lake applyCalculation(Integer id) throws JsonProcessingException {

		Data_lake old = Repository.findById(id).get();

		String url = old.getUrl();

		ResponseEntity<String> response = GETAsString(url);
		String rawBody = response.getBody();

		List<Data_lake> datlakes = Repository.findAllByRefDatlakeId(getUser().getUserId(), id);

		datlakes.forEach(lake -> {
			try {
				Updatejson(lake.getId(), rawBody);
			} catch (JsonProcessingException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		});

		Data_lake saved = Updatejson(id, rawBody);

		System.out.println(" json updated..");
		return saved;

	}

	public Data_lake Updatejson(Integer id, String rawBody) throws JsonProcessingException {

		Data_lake old = Repository.findById(id).get();

//		String url = old.getUrl();
//
//		ResponseEntity<String> response = GETAsString(url);
//		String rawBody = response.getBody();

		// Convert the JSON object (ArrayList, Map, etc.) to a String
//		Object responseBody = response.getBody();
		ObjectMapper mapper = new ObjectMapper();

//		String jsonString = mapper.writeValueAsString(rawBody);
		ObjectMapper jsonMapper = new ObjectMapper();
		Object responseBody;

		// Detect and convert if XML
		if (isXml(rawBody)) {
			try {
				// Convert XML → JSON tree

				XmlMapper xmlMapper = new XmlMapper();

				JsonNode xmlNode = xmlMapper.readTree(rawBody.getBytes());
				// Convert to standard JSON structure
				String jsonFromXml = jsonMapper.writeValueAsString(xmlNode);
				responseBody = jsonMapper.readValue(jsonFromXml, Object.class);
				System.out.println("XML detected and converted to JSON successfully.");
			} catch (Exception e) {
				throw new RuntimeException("Failed to convert XML to JSON: " + e.getMessage());
			}
		} else {
			// Normal JSON response
			try {
				responseBody = jsonMapper.readValue(rawBody, Object.class);
				System.out.println("JSON response detected.");
			} catch (Exception e) {
				throw new RuntimeException("Invalid JSON format: " + e.getMessage());
			}
		}

		// ✅ Handle calculated fields before batch processing
		if (Boolean.TRUE.equals(old.getIscalculatedfield()) && old.getCalculated_field_json() != null) {
			try {
				responseBody = applyCalculatedFields(responseBody, old.getCalculated_field_json(), mapper, old);
				System.out.println("Calculated fields applied successfully.");
			} catch (Exception e) {
				System.err.println("Failed to process calculated fields: " + e.getMessage());
			}
		}

		String jsonString = mapper.writeValueAsString(responseBody);
		old.setJson(jsonString);

		// Process and insert into BatchData as before

		// Process and insert into BatchData
		processBatchData(old, responseBody, mapper);

		String Url = Port_Constant.DOMAIN + "/Data_lake/Data_lake/merge/" + old.getId();
		old.setUrl_endpoint(Url);

		old.setUpdatedBy(getUser().getUserId());

		final Data_lake saved = Repository.save(old);

		System.out.println(" json updated..");
		return saved;

	}

	@SuppressWarnings("unchecked")
	private Object applyCalculatedFields(Object responseBody, String calculatedFieldJson, ObjectMapper mapper,
			Data_lake old) throws JsonProcessingException {

		// Parse the calculated field JSON
		List<Map<String, Object>> calcFields = mapper.readValue(calculatedFieldJson, List.class);

		if (!(responseBody instanceof List)) {
			responseBody = Arrays.asList(responseBody);
		}

		List<Map<String, Object>> records = (List<Map<String, Object>>) responseBody;

		for (Map<String, Object> calc : calcFields) {
			String type = (String) calc.get("type");
			if ("groupby".equalsIgnoreCase(type)) {
				// ✅ Handle group-by aggregation
				List<Map<String, Object>> groupbyrecords = applyGroupBy(records, calc);
				String groupByRecord = mapper.writeValueAsString(groupbyrecords);

				old.setGroupby_json(groupByRecord);

			} else {
				// ✅ Handle calculated / complex operations (as before)
				for (Map<String, Object> record : records) {
					String fieldName = (String) calc.get("fieldName");
					String operation = (String) calc.get("operation");
					List<Map<String, Object>> components = (List<Map<String, Object>>) calc.get("fieldComponents");

					// Handle constant fields
					if (components != null) {
						for (Map<String, Object> comp : components) {
							String subField = (String) comp.get("field");
							Boolean isConstant = comp.get("isConstant") != null && (Boolean) comp.get("isConstant");
							Object constantValue = comp.get("constant");
							if (isConstant && subField != null) {
								record.put(subField, parseNumberOrString(constantValue));
							}
						}
					}

					// Handle complex expression
					if ("complex".equalsIgnoreCase(operation) && calc.get("complexEquation") != null) {
						String equation = (String) calc.get("complexEquation");
						Object result = evaluateComplexExpression(equation, record);
						record.put(fieldName, result);
						continue;
					}

					// Handle normal arithmetic
					List<Object> values = new ArrayList<>();
					if (components != null) {
						for (Map<String, Object> comp : components) {
							Boolean isConstant = comp.get("isConstant") != null && (Boolean) comp.get("isConstant");
							Object val = isConstant ? comp.get("constant") : record.get(comp.get("field"));
							if (val != null) {
								values.add(val);
							}
						}
					}

					Object result = performOperation(values, operation);
					record.put(fieldName, result);
				}
			}
		}

		return records;
	}

	@SuppressWarnings("unchecked")
	private List<Map<String, Object>> applyGroupBy(List<Map<String, Object>> records, Map<String, Object> groupConfig) {
		List<String> groupFields = (List<String>) groupConfig.get("groupFields");
		List<Map<String, Object>> aggregations = (List<Map<String, Object>>) groupConfig.get("aggregations");

		// Group records by key
		Map<String, List<Map<String, Object>>> grouped = new LinkedHashMap<>();
		for (Map<String, Object> record : records) {
			String key = groupFields.stream().map(f -> String.valueOf(record.getOrDefault(f, "")))
					.collect(Collectors.joining("|"));
			grouped.computeIfAbsent(key, k -> new ArrayList<>()).add(record);
		}

		// Aggregate results per group
		List<Map<String, Object>> result = new ArrayList<>();

		for (Map.Entry<String, List<Map<String, Object>>> entry : grouped.entrySet()) {
			Map<String, Object> groupRecord = new LinkedHashMap<>();

			// Add group fields
			String[] parts = entry.getKey().split("\\|");
			for (int i = 0; i < groupFields.size(); i++) {
				groupRecord.put(groupFields.get(i), parts[i]);
			}

			List<Map<String, Object>> groupRows = entry.getValue();

			// Apply each aggregation
			for (Map<String, Object> agg : aggregations) {
				String field = (String) agg.get("field");
				String operation = ((String) agg.get("operation")).toLowerCase();
				List<Double> numericValues = groupRows.stream().map(r -> toDouble(r.get(field)))
						.filter(v -> !Double.isNaN(v)).collect(Collectors.toList());

				Object aggResult;
				switch (operation) {
				case "count":
					aggResult = groupRows.size();
					break;
				case "sum":
					aggResult = numericValues.stream().mapToDouble(Double::doubleValue).sum();
					break;
				case "average":
					aggResult = numericValues.isEmpty() ? 0.0
							: numericValues.stream().mapToDouble(Double::doubleValue).average().orElse(0.0);
					break;
				case "minimum":
					aggResult = numericValues.isEmpty() ? null
							: numericValues.stream().mapToDouble(Double::doubleValue).min().orElse(0.0);
					break;
				case "maximum":
					aggResult = numericValues.isEmpty() ? null
							: numericValues.stream().mapToDouble(Double::doubleValue).max().orElse(0.0);
					break;
				case "median":
					aggResult = calculateMedian(numericValues);
					break;
				case "mode":
					aggResult = calculateMode(numericValues);
					break;
				case "standard deviation":
					aggResult = calculateStdDev(numericValues);
					break;
				default:
					aggResult = null;
				}

				groupRecord.put(field + "_" + operation, aggResult);
			}

			result.add(groupRecord);
		}

		return result;
	}

	// --- Statistical helpers ---

	private Double calculateMedian(List<Double> values) {
		if (values == null || values.isEmpty())
			return null;
		List<Double> sorted = new ArrayList<>(values);
		Collections.sort(sorted);
		int n = sorted.size();
		if (n % 2 == 1) {
			return sorted.get(n / 2);
		} else {
			return (sorted.get(n / 2 - 1) + sorted.get(n / 2)) / 2.0;
		}
	}

	private Double calculateMode(List<Double> values) {
		if (values == null || values.isEmpty())
			return null;
		Map<Double, Long> freq = values.stream().collect(Collectors.groupingBy(v -> v, Collectors.counting()));
		return freq.entrySet().stream().max(Map.Entry.comparingByValue()).map(Map.Entry::getKey).orElse(null);
	}

	private Double calculateStdDev(List<Double> values) {
		if (values == null || values.isEmpty())
			return null;
		double mean = values.stream().mapToDouble(Double::doubleValue).average().orElse(0.0);
		double variance = values.stream().mapToDouble(v -> Math.pow(v - mean, 2)).average().orElse(0.0);
		return Math.sqrt(variance);
	}

	private Object evaluateComplexExpression(String expression, Map<String, Object> record) {
		try {
			// Replace field names with values dynamically
			for (Map.Entry<String, Object> e : record.entrySet()) {
				String field = e.getKey();
				Object val = e.getValue();
				expression = expression.replaceAll("\\b" + field + "\\b", String.valueOf(val != null ? val : 0));
			}

			// Evaluate using ScriptEngine (works on Java 1.8)
			javax.script.ScriptEngine engine = new javax.script.ScriptEngineManager().getEngineByName("JavaScript");
			Object result = engine.eval(expression);
			return result;
		} catch (Exception e) {
			System.err.println("Complex expression error: " + e.getMessage());
			return null;
		}
	}

//	perform normal operation
	private Object performOperation(List<Object> values, String operation) {
		if (values.isEmpty())
			return null;

		switch (operation.toLowerCase()) {
		case "add":
			double sum = 0;
			for (Object v : values)
				sum += toDouble(v);
			return sum;

		case "subtract":
			double result = toDouble(values.get(0));
			for (int i = 1; i < values.size(); i++)
				result -= toDouble(values.get(i));
			return result;

		case "multiply":
			double prod = 1;
			for (Object v : values)
				prod *= toDouble(v);
			return prod;

		case "divide":
			double div = toDouble(values.get(0));
			for (int i = 1; i < values.size(); i++) {
				double val = toDouble(values.get(i));
				if (val != 0)
					div /= val;
			}
			return div;

		case "percentage":
			if (values.size() < 2)
				return null;
			double num = toDouble(values.get(0));
			double den = toDouble(values.get(1));
			return den == 0 ? null : (num / den) * 100;

		case "concat":
			return values.stream().map(Object::toString).collect(Collectors.joining("_"));

		default:
			return null;
		}
	}

	/**
	 * ✅ Evaluates complex equations using JavaScript engine Supports math
	 * operations, parentheses, and string concatenation.
	 */
	private Object evaluateComplexEquation(String expression, Map<String, Object> record,
			List<Map<String, Object>> fieldComponents) {

		if (expression == null || expression.trim().isEmpty())
			return null;

		ScriptEngine engine = new ScriptEngineManager().getEngineByName("JavaScript");

		try {
			// 🔹 Replace constants
			if (fieldComponents != null) {
				for (Map<String, Object> comp : fieldComponents) {
					String field = (String) comp.get("field");
					Object constant = comp.get("constant");
					Boolean isConstant = comp.get("isConstant") != null && (Boolean) comp.get("isConstant");

					if (isConstant && constant != null) {
						String constVal = constant.toString();
						expression = expression.replaceAll("\\b" + field + "\\b", constVal);
					}
				}
			}

			// 🔹 Replace field values from record
			for (Map.Entry<String, Object> entry : record.entrySet()) {
				String key = entry.getKey();
				Object val = entry.getValue();
				if (val != null) {
					String safeValue = val.toString();

					// If it's a string containing letters, wrap it in quotes for JS
					if (!safeValue.matches("^-?\\d+(\\.\\d+)?$")) {
						safeValue = "'" + safeValue.replace("'", "\\'") + "'";
					}

					expression = expression.replaceAll("\\b" + key + "\\b", safeValue);
				}
			}

			// Evaluate the expression safely
			Object result = engine.eval(expression);
			System.out.println(" exoression is : " + expression + "  and " + result);
			return result;

		} catch (Exception e) {
			System.err.println("❌ Error evaluating complex equation: " + e.getMessage());
			return null;
		}
	}

	private double toDouble(Object val) {
		if (val == null)
			return 0.0;
		try {
			return Double.parseDouble(val.toString().trim());
		} catch (NumberFormatException e) {
			return 0.0;
		}
	}

	private Object parseNumberOrString(Object val) {
		if (val == null)
			return null;
		String str = val.toString().trim();
		try {
			return Double.parseDouble(str);
		} catch (NumberFormatException e) {
			return str;
		}
	}

	private boolean isXml(String content) {
		if (content == null)
			return false;
		String trimmed = content.trim();
		// XML usually starts with '<' and ends with '>'
		return trimmed.startsWith("<") && trimmed.endsWith(">");
	}

	private void processBatchData(Data_lake dataLake, Object responseBody, ObjectMapper mapper)
			throws JsonProcessingException {

		int batchVolume = (dataLake.getBatch_volume() != null && dataLake.getBatch_volume() > 0)
				? dataLake.getBatch_volume()
				: 100; // default batch size if not given

		// Convert to JsonNode
		JsonNode jsonNode = mapper.valueToTree(responseBody);

		if (jsonNode.isArray()) {
			ArrayNode arrayNode = (ArrayNode) jsonNode;
			int total = arrayNode.size();
			System.out.println("Total records: " + total);

			for (int i = 0; i < total; i += batchVolume) {
				int end = Math.min(i + batchVolume, total);

				// Create a sub-array manually
				ArrayNode subArray = mapper.createArrayNode();
				for (int j = i; j < end; j++) {
					subArray.add(arrayNode.get(j));
				}

				String subJson = mapper.writeValueAsString(subArray);

				BatchData batch = new BatchData();
				batch.setDatalake_id(dataLake.getId());
				batch.setBatchjson(subJson);
				batchRepo.save(batch);
			}

			System.out.println("Inserted " + (int) Math.ceil((double) total / batchVolume) + " batch records.");

		} else {
			// Single object → one record
			BatchData batch = new BatchData();
			batch.setDatalake_id(dataLake.getId());
			batch.setBatchjson(mapper.writeValueAsString(jsonNode));
			batchRepo.save(batch);

			System.out.println("Inserted single batch record.");
		}
	}

	public String mergeBatchData(Integer datalakeId) throws Exception {
		List<BatchData> batchList = batchRepo.findByDatalake_id(datalakeId);

		if (batchList.isEmpty()) {
			throw new RuntimeException("No batch data found for datalake_id: " + datalakeId);
		}

		ObjectMapper mapper = new ObjectMapper();
		ArrayNode mergedArray = mapper.createArrayNode();

		for (BatchData batch : batchList) {
			String json = batch.getBatchjson();
			JsonNode node = mapper.readTree(json);

			if (node.isArray()) {
				// Add each element to merged array
				for (JsonNode item : node) {
					mergedArray.add(item);
				}
			} else {
				// Single object, just add directly
				mergedArray.add(node);
			}
		}

		String mergedJson = mapper.writeValueAsString(mergedArray);
		System.out.println("Merged JSON size: " + mergedArray.size());
		return mergedJson;
	}

	public ResponseEntity<Object> GET(String get) {
		RestTemplate restTemplate = new RestTemplate();

		ResponseEntity<Object> u = restTemplate.getForEntity(get, Object.class);

		return u;

	}

	public ResponseEntity<String> GETAsString(String url) {
		RestTemplate restTemplate = new RestTemplate();
		return restTemplate.getForEntity(url, String.class);
	}

	public AppUser getUser() {
		AppUser user = userService.getLoggedInUser();
		return user;

	}

}
