package de.avatic.lcc.service.api;

import de.avatic.lcc.model.azuremaps.geocoding.batch.BatchGeocodingRequest;
import de.avatic.lcc.model.azuremaps.geocoding.batch.BatchGeocodingResponse;
import de.avatic.lcc.model.azuremaps.geocoding.batch.BatchItem;
import de.avatic.lcc.model.azuremaps.geocoding.fuzzy.FuzzySearchResponse;
import de.avatic.lcc.model.bulk.BulkInstruction;
import de.avatic.lcc.model.db.country.IsoCode;
import de.avatic.lcc.model.excel.ExcelNode;
import de.avatic.lcc.util.exception.internalerror.ExcelValidationError;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;

import java.math.BigDecimal;
import java.net.URI;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

@Service
public class BatchGeoApiService {

    private static final Logger logger = LoggerFactory.getLogger(BatchGeoApiService.class);
    private static final String AZURE_MAPS_BATCH_GEOCODING_URL = "https://atlas.microsoft.com/geocode:batch";
    private static final int MAX_BATCH_SIZE = 100; // Azure Maps batch limit

    private final RestTemplate restTemplate;
    private final String subscriptionKey;

    public BatchGeoApiService(RestTemplate restTemplate,
                              @Value("${azure.maps.subscription.key}") String subscriptionKey) {
        this.restTemplate = restTemplate;
        this.subscriptionKey = subscriptionKey;
    }


    public void geocodeBatch(List<BulkInstruction<ExcelNode>> nodes) {
        if (nodes == null || nodes.isEmpty()) {
            logger.warn("Address list is null or empty");
            return;
        }

        ArrayList<BulkInstruction<ExcelNode>> noGeo = new ArrayList<>();
        ArrayList<BulkInstruction<ExcelNode>> failedGeoLookups = new ArrayList<>();
        ArrayList<BulkInstruction<ExcelNode>> failedFuzzyGeoLookups = new ArrayList<>();
        int totalSuccessful = 0;

        for (var node : nodes) {
            if (node.getEntity().getGeoLat() == null || node.getEntity().getGeoLng() == null) {
                noGeo.add(node);
            }
        }

        for (int currentBatch = 0; currentBatch < noGeo.size(); currentBatch += MAX_BATCH_SIZE) {
            int end = Math.min(currentBatch + MAX_BATCH_SIZE, noGeo.size());
            var chunk = noGeo.subList(currentBatch, end);

            var chunkResult = executeBatchRequest(chunk.stream()
                    .map(BulkInstruction::getEntity).map(this::getGeoCodeString).map(BatchItem::new)
                    .toList());

            if (chunkResult.isPresent()) {
                var response = chunkResult.get();


                if (response.getSummary() != null && response.getSummary().getSuccessfulRequests() != null) {
                    totalSuccessful += response.getSummary().getSuccessfulRequests();
                }

                if (response.getBatchItems() == null || response.getBatchItems().isEmpty()) {
                    logger.warn("Batch response contains no items");
                    failedGeoLookups.addAll(chunk);
                    continue;
                }

                for (int itemIdx = 0; itemIdx < chunk.size(); itemIdx++) {

                    if (itemIdx >= response.getBatchItems().size()) {
                        logger.warn("BatchItems size mismatch at index {}", itemIdx);
                        failedGeoLookups.add(chunk.get(itemIdx));
                        continue;
                    }

                    var result = response.getBatchItems().get(itemIdx);
                    var node = chunk.get(itemIdx).getEntity();


                    if (result == null || result.getFeatures() == null || result.getFeatures().isEmpty()) {
                        logger.warn("No geocoding result for address {}",
                                node.getAddress() != null ? node.getAddress() : "unknown");
                        failedGeoLookups.add(chunk.get(itemIdx));
                        continue;
                    }

                    var feature = result.getFeatures().getFirst();
                    if (feature == null) {
                        logger.warn("Feature is null for address {}", node.getAddress());
                        failedGeoLookups.add(chunk.get(itemIdx));
                        continue;
                    }

                    var properties = feature.getProperties();
                    if (properties == null) {
                        logger.warn("Properties is null for address {}", node.getAddress());
                        failedGeoLookups.add(chunk.get(itemIdx));
                        continue;
                    }

                    String confidence = properties.getConfidence();
                    boolean hasGoodConfidence = confidence != null &&
                            (confidence.equalsIgnoreCase("high") ||
                                    confidence.equalsIgnoreCase("medium"));

                    boolean hasGoodMatchCode = properties.getMatchCodes() != null &&
                            properties.getMatchCodes().stream()
                                    .anyMatch(s -> s != null && s.equalsIgnoreCase("good"));

                    if (hasGoodConfidence || hasGoodMatchCode) {
                        var geometry = feature.getGeometry();
                        if (geometry == null || geometry.getCoordinates() == null ||
                                geometry.getCoordinates().size() < 2) {
                            logger.warn("Invalid geometry for address {}", node.getAddress());
                            failedGeoLookups.add(chunk.get(itemIdx));
                            continue;
                        }

                        var coordinates = geometry.getCoordinates();
                        if (coordinates.get(0) == null || coordinates.get(1) == null) {
                            logger.warn("Null coordinates for address {}", node.getAddress());
                            failedGeoLookups.add(chunk.get(itemIdx));
                            continue;
                        }

                        node.setGeoLng(BigDecimal.valueOf(coordinates.get(0)));
                        node.setGeoLat(BigDecimal.valueOf(coordinates.get(1)));

                        if (properties.getAddress() != null &&
                                properties.getAddress().getFormattedAddress() != null) {
                            node.setAddress(properties.getAddress().getFormattedAddress());
                        }

                        if (properties.getAddress() != null &&
                                properties.getAddress().getCountryRegion() != null &&
                                properties.getAddress().getCountryRegion().getIso() != null) {
                            try {
                                node.setCountryId(IsoCode.valueOf(
                                        properties.getAddress().getCountryRegion().getIso()));
                            } catch (IllegalArgumentException e) {
                                logger.warn("Invalid ISO code: {}",
                                        properties.getAddress().getCountryRegion().getIso());
                            }
                        }
                    } else {
                        logger.warn("Geocoding failed for address {} (low confidence)",
                                node.getAddress());
                        failedGeoLookups.add(chunk.get(itemIdx));
                    }
                }
            } else {
                logger.warn("Batch request returned empty result");
                failedGeoLookups.addAll(chunk);
            }
        }


        // Second pass: fuzzy lookup with company name for failed addresses
        if (!failedGeoLookups.isEmpty()) {
            logger.info("Retrying {} failed lookups with fuzzy search", failedGeoLookups.size());
            int fuzzySuccessful = 0;

            for (var instruction : failedGeoLookups) {
                var node = instruction.getEntity();
                var fuzzyResult = executeFuzzySearch(node);

                if (fuzzyResult.isPresent() && fuzzyResult.get().getResults() != null
                        && !fuzzyResult.get().getResults().isEmpty()) {

                    var result = fuzzyResult.get().getResults().getFirst();
                    if (result == null) {
                        logger.warn("Fuzzy result is null for: {}", node.getName());
                        failedFuzzyGeoLookups.add(instruction);
                        continue;
                    }

                    double score = result.getScore();
                    if (score < 7.0) {
                        logger.warn("Fuzzy search returned low confidence result for: {} (score: {})",
                                node.getName(), score);
                        failedFuzzyGeoLookups.add(instruction);
                        continue;
                    }

                    if (result.getPosition() == null) {
                        logger.warn("Position is null for: {}", node.getName());
                        failedFuzzyGeoLookups.add(instruction);
                        continue;
                    }

                    double lat = result.getPosition().getLat();
                    double lon = result.getPosition().getLon();

                    node.setGeoLat(BigDecimal.valueOf(lat));
                    node.setGeoLng(BigDecimal.valueOf(lon));

                    if (result.getAddress() != null &&
                            result.getAddress().getFreeformAddress() != null) {
                        node.setAddress(result.getAddress().getFreeformAddress());
                    }

                    if (result.getAddress() != null &&
                            result.getAddress().getCountryCode() != null) {
                        try {
                            node.setCountryId(IsoCode.valueOf(result.getAddress().getCountryCode()));
                        } catch (IllegalArgumentException e) {
                            logger.warn("Unknown country code: {}",
                                    result.getAddress().getCountryCode());
                            failedFuzzyGeoLookups.add(instruction);
                            continue;
                        }
                    }

                    fuzzySuccessful++;
                    logger.info("Fuzzy search successful for: {} (score: {})",
                            node.getName(), score);
                }
            }

            logger.info("Fuzzy lookup recovered {} of {} failed addresses",
                    fuzzySuccessful, failedGeoLookups.size());

            // Throw error for remaining failed lookups
            if (!failedFuzzyGeoLookups.isEmpty()) {

                failedFuzzyGeoLookups.forEach(instruction -> {logger.warn("Lookup finally failed for: {}", instruction.getEntity().getName());});

                var firstFailed = failedGeoLookups.stream()
                        .filter(i -> i.getEntity().getGeoLat() == null)
                        .findFirst()
                        .map(BulkInstruction::getEntity)
                        .orElse(null);

                if (firstFailed != null) {
                    String name = firstFailed.getName() != null ?
                            firstFailed.getName() : "unknown";
                    throw new ExcelValidationError("Unable to geocode " + name
                            + ". Please check your address or enter geo position yourself.");
                }
            }
        }
    }

    private Optional<FuzzySearchResponse> executeFuzzySearch(ExcelNode node) {
        try {
            String companyName = node.getName();
            if (companyName == null) {
                logger.warn("Company name is null for fuzzy search");
                return Optional.empty();
            }

            IsoCode countryId = node.getCountryId();
            if (countryId == null) {
                logger.warn("Country ID is null for fuzzy search: {}", companyName);
                return Optional.empty();
            }
            String country = countryId.name();

            String address = node.getAddress();
            if (address == null) {
                logger.warn("Address is null for fuzzy search: {}", companyName);
                address = ""; // Fallback zu leerem String
            }

            // Normalisiere Unicode für konsistente Suche
            companyName = java.text.Normalizer.normalize(companyName,
                    java.text.Normalizer.Form.NFC);

            // URL-Encoding
            String encodedQuery = URLEncoder.encode(
                    companyName + ", " + address + ", " + country,
                    StandardCharsets.UTF_8);

            String url = String.format(
                    "https://atlas.microsoft.com/search/fuzzy/json?api-version=1.0&subscription-key=%s&query=%s&limit=5",
                    subscriptionKey,
                    encodedQuery
            );

            URI uri = URI.create(url);

            logger.debug("Fuzzy search for: {} (normalized & encoded)", companyName);

            ResponseEntity<FuzzySearchResponse> response = restTemplate.getForEntity(
                    uri,
                    FuzzySearchResponse.class
            );

            return Optional.ofNullable(response.getBody());

        } catch (Exception e) {
            logger.error("Fuzzy search failed for {}",
                    node.getName() != null ? node.getName() : "unknown", e);
            return Optional.empty();
        }
    }

    private String getGeoCodeString(ExcelNode excelNode) {
        String address = excelNode.getAddress();
        IsoCode countryId = excelNode.getCountryId();

        // Fallback-Werte für null
        String addressStr = address != null ? address : "";
        String countryStr = countryId != null ? countryId.name() : "";

        return addressStr + ", " + countryStr;
    }

    private Optional<BatchGeocodingResponse> executeBatchRequest(List<BatchItem> batchItems) {
        try {
            URI uri = UriComponentsBuilder.fromUriString(AZURE_MAPS_BATCH_GEOCODING_URL)
                    .queryParam("api-version", "2025-01-01")
                    .queryParam("subscription-key", subscriptionKey)
                    .build()
                    .toUri();

            BatchGeocodingRequest request = new BatchGeocodingRequest(batchItems);

            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_JSON);

            HttpEntity<BatchGeocodingRequest> entity = new HttpEntity<>(request, headers);

            logger.debug("Calling Azure Maps Batch API for {} addresses", batchItems.size());

            ResponseEntity<BatchGeocodingResponse> responseEntity = restTemplate.exchange(
                    uri,
                    HttpMethod.POST,
                    entity,
                    BatchGeocodingResponse.class
            );

            return Optional.ofNullable(responseEntity.getBody());

        } catch (Exception e) {
            logger.error("Failed to execute batch geocoding request", e);
            throw new RuntimeException("Batch geocoding failed", e);
        }
    }

}