/*
 * File: src/main/java/reports/service/SupplierService.java
 * Description: This service class provides functionality for handling supplier-related operations in the ERP reporting system.
 * It includes methods for retrieving KPI data and generating supplier reports based on sales and purchase data.
 */

package com.nebula.erp.reports.service;

import com.nebula.erp.reports.model.purchase.*;
import com.nebula.erp.reports.repository.product.BrandRepository;
import com.nebula.erp.reports.repository.product.CategoryRepository;
import com.nebula.erp.reports.repository.purchase.*;
import com.nebula.erp.reports.repository.sales.SalesRepository;
import com.nebula.erp.reports.repository.sales.SalesReturnItemRepository;
import com.nebula.erp.reports.repository.sales.SalesItemRepository;
import com.nebula.erp.reports.repository.product.ProductRepository;
import com.nebula.erp.reports.requestmodel.PurchaseRequest;
import com.nebula.erp.reports.utility.CreateLogger;
import com.nebula.erp.reports.utility.JwtRequestUtils;
import com.nebula.erp.reports.utility.MoneyUtils;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import com.nebula.erp.reports.utility.ApiResponseStructure;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.*;

@Service
public class SupplierService {

    @Autowired
    private PurchaseRepository purchaseRepository;

    @Autowired
    private SalesRepository salesRepository;

    @Autowired
    private SalesItemRepository salesItemRepository;

    @Autowired
    private SalesReturnItemRepository salesReturnItemRepository;

    @Autowired
    private SupplierRepository supplierRepository;

    @Autowired
    private GRNRepository grnRepository;

    @Autowired
    private PurchaseReturnRepository purchaseReturnRepository;

    @Autowired
    private PurchaseReturnItemRepository purchaseReturnItemRepository;

    @Autowired
    private ProductRepository productRepository;

    @Autowired
    private GRNItemRepository grnItemRepository;

    @Autowired
    private PurchaseItemRepository purchaseItemRepository;

    @Autowired
    private CategoryRepository categoryRepository;

    @Autowired
    private BrandRepository brandRepository;

    @Autowired
    private JwtRequestUtils jwtRequestUtils;

    @Autowired
    private HttpServletRequest httpServletRequest;

    @Autowired
    private CreateLogger createLogger;

    private static final String path = "/reports/supplier";

    public ApiResponseStructure<Map<String, Object>> getKPIData(LocalDate fromDate, LocalDate toDate) {
        // Extract Authorization headers from the request
        HttpHeaders headers = new HttpHeaders();
        headers.add(HttpHeaders.AUTHORIZATION, httpServletRequest.getHeader(HttpHeaders.AUTHORIZATION));

        String tenantName = jwtRequestUtils.getTenantNameFromHeaders(headers);

        if (fromDate != null && toDate != null) {
            // Convert LocalDate to LocalDateTime for comparison with created_at
            LocalDateTime fromDateTime = fromDate.atStartOfDay();
            LocalDateTime toDateTime = toDate.atTime(23, 59, 59);

            Long totalSuppliers = supplierRepository.countTotalSuppliersForTenant(tenantName, fromDateTime, toDateTime);
            Long totalReturns = purchaseReturnRepository.countTotalPurchaseReturns(tenantName, fromDateTime, toDateTime);

            List<Long> supplierIds = purchaseRepository.findSupplierWithMostPurchases(fromDateTime, toDateTime, tenantName);
            Long topSupplierId = supplierIds.isEmpty() ? null : supplierIds.get(0);
            String topSupplierName = null;
            if (topSupplierId != null) {
                topSupplierName = supplierRepository.findSupplierNameById(topSupplierId, tenantName);
            }

            // Calculate on-time delivery rate and average delivery time
            Long onTimeDeliveryCount = purchaseRepository.countOnTimeDeliveries(fromDateTime, toDateTime, tenantName);
            Long totalDeliveryCount = purchaseRepository.countTotalDeliveries(fromDateTime, toDateTime, tenantName);

            BigDecimal onTimeDeliveryRate = totalDeliveryCount > 0 ? BigDecimal.valueOf(onTimeDeliveryCount).divide(BigDecimal.valueOf(totalDeliveryCount), 2, RoundingMode.HALF_UP).multiply(BigDecimal.valueOf(100)) : BigDecimal.ZERO; // avoid division by zero

            // Format the on_time_delivery_rate
            String formattedOnTimeDeliveryRate = onTimeDeliveryRate.setScale(2, RoundingMode.HALF_UP) + "%";

            // Calculate average delivery time
            BigDecimal averageDeliveryTime = purchaseRepository.calculateAverageDeliveryTime(fromDateTime, toDateTime, tenantName);
            String formattedAverageDeliveryTime = (averageDeliveryTime != null ? averageDeliveryTime.multiply(BigDecimal.valueOf(24)).setScale(2, RoundingMode.HALF_UP) + " hours" : "N/A"); // Use a default value or message if null

            // Create the response map
            Map<String, Object> purchaseData = new LinkedHashMap<>();
            purchaseData.put("total_suppliers", totalSuppliers);
            purchaseData.put("top_supplier_by_orders", topSupplierName);
            purchaseData.put("on_time_delivery_rate", formattedOnTimeDeliveryRate);
            purchaseData.put("total_returns", totalReturns);
            purchaseData.put("average_delivery_time", formattedAverageDeliveryTime);

            // Create and return the structured ApiResponseStructure
            return new ApiResponseStructure<>("Success", 200, "Data retrieved.", purchaseData);
        }
        createLogger.createLogger("error", path, "GET", "Please provide start-date and end-date with valid dates.", "");
        return new ApiResponseStructure<>("Failure", 500, "Please provide start-date and end-date with valid dates.", Collections.emptyMap());
    }

    public ApiResponseStructure<Map<String, Object>> getPurchaseReport(PurchaseRequest purchaseRequest, String reportType) {
        // Extract Authorization headers from the request
        HttpHeaders headers = new HttpHeaders();
        headers.add(HttpHeaders.AUTHORIZATION, httpServletRequest.getHeader(HttpHeaders.AUTHORIZATION));

        String tenantName = jwtRequestUtils.getTenantNameFromHeaders(headers);

        if (purchaseRequest.getStartDate() == null || purchaseRequest.getEndDate() == null) {
            createLogger.createLogger("error", path, "POST", "Both 'startDate' and 'endDate' must be provided.", "");
            return new ApiResponseStructure<>("Error", HttpStatus.BAD_REQUEST.value(), "Both 'startDate' and 'endDate' must be provided.", new HashMap<>());
        }

        Specification<GRNItem> spec = Specification.where(null);
        spec = spec.and((root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get("tenant"), tenantName));

        LocalDate fromDate = purchaseRequest.getStartDate(); // Ensure this is LocalDate
        LocalDate toDate = purchaseRequest.getEndDate();     // Ensure this is LocalDate

        LocalDateTime fromDateTime = fromDate.atStartOfDay();
        LocalDateTime toDateTime = toDate.atTime(23, 59, 59);

        spec = spec.and((root, query, criteriaBuilder) -> criteriaBuilder.between(root.get("created_at"), fromDateTime, toDateTime));

        // Switch cases to handle various report type
        switch (reportType) {
            case "byPerformance":
                // Add dynamic conditions
                if (purchaseRequest.getConditions() != null) {
                    for (PurchaseRequest.Condition condition : purchaseRequest.getConditions()) {
                        switch (condition.getOperator()) {
                            case "equals":
                                spec = spec.and((root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get(condition.getField()), condition.getValue()));
                                break;
                            case "greaterThan":
                                spec = spec.and((root, query, criteriaBuilder) -> criteriaBuilder.greaterThan(root.get(condition.getField()), (Comparable) condition.getValue()));
                                break;
                            case "lessThan":
                                spec = spec.and((root, query, criteriaBuilder) -> criteriaBuilder.lessThan(root.get(condition.getField()), (Comparable) condition.getValue()));
                                break;
                            case "greaterThanOrEquals":
                                spec = spec.and((root, query, criteriaBuilder) -> criteriaBuilder.greaterThanOrEqualTo(root.get(condition.getField()), (Comparable) condition.getValue()));
                                break;
                            case "lessThanOrEquals":
                                spec = spec.and((root, query, criteriaBuilder) -> criteriaBuilder.lessThanOrEqualTo(root.get(condition.getField()), (Comparable) condition.getValue()));
                                break;
                            default:
                                createLogger.createLogger("error", path, "POST", "Operator " + condition.getOperator() + " is not supported.", "");
                                throw new IllegalArgumentException("Operator " + condition.getOperator() + " is not supported.");
                        }
                    }
                }

                // Fetch the data with pagination for GRN items
                Pageable pageable = PageRequest.of(purchaseRequest.getPage() != null ? purchaseRequest.getPage() : 0, // Page number is zero-based
                        purchaseRequest.getSize() != null ? purchaseRequest.getSize() : 20);

                // Fetch all GRNItems based on the specification
                Page<GRNItem> grnItemPage = grnItemRepository.findAll(spec, pageable);

                // Group purchases and GRNs by supplier
                Map<Long, Map<String, Object>> performanceData = new HashMap<>();

                // Iterate through each GRNItem and accumulate quantities and total prices
                grnItemPage.getContent().forEach(item -> {
                    Long supplierId = item.getGrn_item().getSupplier().getId(); // Get supplier ID from GRN
                    String supplierName = item.getGrn_item().getSupplier().getSupplier_name(); // Get supplier name from GRN

                    // Initialize supplier data if it doesn't exist
                    performanceData.computeIfAbsent(supplierId, id -> {
                        Map<String, Object> data = new LinkedHashMap<>();
                        data.put("supplier_id", supplierId.toString()); // Convert supplierId to String
                        data.put("supplier_name", supplierName);
                        data.put("purchases", new ArrayList<Map<String, Object>>());
                        return data;
                    });

                    // Prepare purchase details for GRN
                    Map<String, Object> purchaseDataEntry = new LinkedHashMap<>();
                    List<Map<String, Object>> grnList = new ArrayList<>(); // List to hold GRN entries

                    // Prepare GRN entry
                    Map<String, Object> grnDataEntry = new LinkedHashMap<>();
                    grnDataEntry.put("grn_item_id", item.getId()); // GRN ID
                    grnDataEntry.put("grn_date", String.valueOf(item.getGrn_item().getDate())); // GRN Date
                    grnDataEntry.put("grn_quantity", item.getQuantity()); // GRN Quantity
                    grnDataEntry.put("grn_total_price", MoneyUtils.truncateToTwoDecimals(item.getTotal_price())); // GRN Total Price

                    // Fetch Purchase data for the same supplier and within the date range
                    List<Purchase> purchases = purchaseRepository.findAllPurchasesByDateRangeAndSupplier(fromDateTime, toDateTime, supplierId, tenantName);
                    purchases.forEach(purchase -> {
                        purchaseDataEntry.put("purchase_id", purchase.getId()); // Purchase ID
                        purchaseDataEntry.put("purchase_date", String.valueOf(purchase.getDate())); // Purchase Date

                        // Calculate date difference (in days)
                        long dateDifference = java.time.Duration.between(purchase.getDate(), item.getGrn_item().getDate()).toDays();
                        purchaseDataEntry.put("date_difference", dateDifference); // Date Difference
                    });

                    Long productId = item.getProduct_id();
                    String productName = productRepository.findProductNameById(productId, tenantName); // Fetch product name
                    Long categoryId = productRepository.findCategoryIdByProductId(productId, tenantName); // Fetch category ID
                    Long brandId = productRepository.findBrandIdByProductId(productId, tenantName); // Fetch brand ID
                    String categoryName = categoryRepository.findCategoryNameById(categoryId, tenantName); // Fetch category name
                    String brandName = brandRepository.findBrandNameById(brandId, tenantName); // Fetch brand name

                    // Add product data to the GRN entry
                    grnDataEntry.put("product_id", productId);
                    grnDataEntry.put("product_name", productName);
                    grnDataEntry.put("category_id", categoryId);
                    grnDataEntry.put("category_name", categoryName);
                    grnDataEntry.put("brand_id", brandId);
                    grnDataEntry.put("brand_name", brandName);

                    // Add the GRN entry to the list
                    grnList.add(grnDataEntry);

                    // If the purchaseDataEntry doesn't already exist in performanceData, initialize it
                    if (!((List<Map<String, Object>>) performanceData.get(supplierId).get("purchases")).contains(purchaseDataEntry)) {
                        purchaseDataEntry.put("grns", grnList); // Attach the GRN list to purchase entry
                        ((List<Map<String, Object>>) performanceData.get(supplierId).get("purchases")).add(purchaseDataEntry);
                    } else {
                        // If it exists, just update the existing purchase entry
                        Map<String, Object> existingPurchaseEntry = ((List<Map<String, Object>>) performanceData.get(supplierId).get("purchases")).stream().filter(p -> p.get("purchase_id").equals(purchaseDataEntry.get("purchase_id"))).findFirst().orElseThrow(() -> new RuntimeException("Existing purchase entry not found."));

                        List<Map<String, Object>> existingGrns = (List<Map<String, Object>>) existingPurchaseEntry.get("grns");
                        existingGrns.add(grnDataEntry); // Add the new GRN entry to existing purchase entry
                    }
                });

                // Prepare reportData list from the performance data
                List<Map<String, Object>> performanceDataList = new ArrayList<>(performanceData.values());

                // Prepare column details
                Map<String, Object> column = new LinkedHashMap<>();
                column.put("supplier_id", "SUPPLIER ID");
                column.put("supplier_name", "SUPPLIER NAME");
                column.put("purchase_id", "PURCHASE ID");
                column.put("purchase_date", "PURCHASE DATE");
                column.put("date_difference", "DATE DIFFERENCE (DAYS)");
                column.put("grn_item_id", "GRN ITEM ID");
                column.put("grn_date", "GRN DATE");
                column.put("grn_quantity", "GRN QUANTITY");
                column.put("grn_total_price", "GRN TOTAL PRICE");
                column.put("product_id", "PRODUCT ID");
                column.put("product_name", "PRODUCT NAME");
                column.put("category_id", "CATEGORY ID");
                column.put("category_name", "CATEGORY NAME");
                column.put("brand_id", "BRAND ID");
                column.put("brand_name", "BRAND NAME");

                // Prepare pagination details
                Map<String, Object> pagination = new LinkedHashMap<>();
                pagination.put("currentPage", grnItemPage.getNumber() + 1); // Page number is 0-based
                pagination.put("totalPages", grnItemPage.getTotalPages());
                pagination.put("pageSize", pageable.getPageSize());
                pagination.put("totalRecords", performanceDataList.size());

                // Prepare summary details
                long totalSuppliers = performanceData.size();

                // Calculate on-time delivery rate
                long onTimeDeliveries = performanceDataList.stream()
                        .flatMap(supplier -> ((List<Map<String, Object>>) supplier.get("purchases")).stream())
                        .filter(purchase -> {
                            long dateDifference = (Long) purchase.get("date_difference");
                            return dateDifference <= 0; // On-time if date difference is 0 or less
                        }).count();

                long totalDeliveries = performanceDataList.stream()
                        .flatMap(supplier -> ((List<Map<String, Object>>) supplier.get("purchases")).stream())
                        .count();

                double onTimeDeliveryRate = totalDeliveries > 0 ? (double) onTimeDeliveries / totalDeliveries * 100 : 0;

                // Prepare summary details
                Map<String, Object> summary = new LinkedHashMap<>();
                summary.put("totalSuppliers", totalSuppliers);
                summary.put("onTimeDeliveryRate", Math.round(onTimeDeliveryRate * 100.0) / 100.0); // Round to two decimal places

                // Prepare response data
                Map<String, Object> purchaseData = new LinkedHashMap<>();
                purchaseData.put("module", "supplier");
                purchaseData.put("reportType", reportType);
                purchaseData.put("startDate", String.valueOf(fromDate));
                purchaseData.put("endDate", String.valueOf(toDate));
                purchaseData.put("reportData", performanceDataList);
                purchaseData.put("column", column);
                purchaseData.put("pagination", pagination);
                purchaseData.put("summary", summary);

                // Create and return the structured ApiResponseStructure
                return new ApiResponseStructure<>("Success", 200, "Data retrieved.", purchaseData);

            case "purchaseHistory":
                Specification<Purchase> specs = Specification.where(null);
                specs = specs.and((root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get("tenant"), tenantName));

                // Fetch the data with pagination for GRN items
                pageable = PageRequest.of(purchaseRequest.getPage() != null ? purchaseRequest.getPage() : 0, // Page number is zero-based
                        purchaseRequest.getSize() != null ? purchaseRequest.getSize() : 20);

                // Add dynamic conditions
                if (purchaseRequest.getConditions() != null) {
                    for (PurchaseRequest.Condition condition : purchaseRequest.getConditions()) {
                        switch (condition.getOperator()) {
                            case "equals":
                                specs = specs.and((root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get(condition.getField()), condition.getValue()));
                                break;
                            case "greaterThan":
                                specs = specs.and((root, query, criteriaBuilder) -> criteriaBuilder.greaterThan(root.get(condition.getField()), (Comparable) condition.getValue()));
                                break;
                            case "lessThan":
                                specs = specs.and((root, query, criteriaBuilder) -> criteriaBuilder.lessThan(root.get(condition.getField()), (Comparable) condition.getValue()));
                                break;
                            case "greaterThanOrEquals":
                                specs = specs.and((root, query, criteriaBuilder) -> criteriaBuilder.greaterThanOrEqualTo(root.get(condition.getField()), (Comparable) condition.getValue()));
                                break;
                            case "lessThanOrEquals":
                                specs = specs.and((root, query, criteriaBuilder) -> criteriaBuilder.lessThanOrEqualTo(root.get(condition.getField()), (Comparable) condition.getValue()));
                                break;
                            default:
                                createLogger.createLogger("error", path, "POST", "Operator " + condition.getOperator() + " is not supported.", "");
                                throw new IllegalArgumentException("Operator " + condition.getOperator() + " is not supported.");
                        }
                    }
                }

                specs = specs.and((root, query, criteriaBuilder) -> criteriaBuilder.between(root.get("created_at"), fromDateTime, toDateTime));

                // Fetch all purchases for the tenant within the date range
                Page<Purchase> purchaseHistory = purchaseRepository.findAll(specs, pageable);

                // Group purchases by supplier
                Map<Long, Map<String, Object>> supplierDataMap = new LinkedHashMap<>();
                purchaseHistory.forEach(purchase -> {
                    Long supplierId = purchase.getSupplier().getId();
                    String supplierName = purchase.getSupplier().getSupplier_name();

                    // Initialize supplier entry if not present
                    supplierDataMap.computeIfAbsent(supplierId, id -> {
                        Map<String, Object> data = new LinkedHashMap<>();
                        data.put("supplier_id", supplierId.toString()); // Convert to String
                        data.put("supplier_name", supplierName);
                        data.put("purchases", new ArrayList<Map<String, Object>>());
                        return data;
                    });

                    // Prepare purchase entry
                    Map<String, Object> purchaseEntry = new LinkedHashMap<>();
                    purchaseEntry.put("purchase_id", purchase.getId());
                    purchaseEntry.put("purchase_date", String.valueOf(purchase.getDate()));

                    // Fetch associated GRNItems for the purchase
                    List<GRNItem> grnItems = grnItemRepository.findByPurchaseOrderIdAndTenant(purchase.getId(), tenantName); // Assuming you have this method

                    List<Map<String, Object>> grnList = new ArrayList<>();
                    for (GRNItem grnItem : grnItems) {
                        Map<String, Object> grnEntry = new LinkedHashMap<>();
                        grnEntry.put("grn_item_id", grnItem.getId()); // Assuming there's a method to get the GRN from GRNItem
                        grnEntry.put("grn_date", String.valueOf(grnItem.getGrn_item().getDate())); // GRN Date
                        grnEntry.put("grn_quantity", grnItem.getQuantity()); // Quantity from GRNItem
                        grnEntry.put("grn_total_price", grnItem.getTotal_price()); // Total price from GRNItem

                        Long productId = grnItem.getProduct_id();
                        String productName = productRepository.findProductNameById(productId, tenantName); // Fetch product name
                        Long categoryId = productRepository.findCategoryIdByProductId(productId, tenantName); // Fetch category ID
                        Long brandId = productRepository.findBrandIdByProductId(productId, tenantName); // Fetch brand ID
                        String categoryName = categoryRepository.findCategoryNameById(categoryId, tenantName); // Fetch category name
                        String brandName = brandRepository.findBrandNameById(brandId, tenantName); // Fetch brand name

                        grnEntry.put("product_id", productId);
                        grnEntry.put("product_name", productName);
                        grnEntry.put("category_id", categoryId);
                        grnEntry.put("category_name", categoryName);
                        grnEntry.put("brand_id", brandId);
                        grnEntry.put("brand_name", brandName);

                        grnList.add(grnEntry);
                    }

                    // Add GRNs to the purchase entry
                    purchaseEntry.put("grns", grnList);

                    // Add purchase entry to the supplier's purchases
                    ((List<Map<String, Object>>) supplierDataMap.get(supplierId).get("purchases")).add(purchaseEntry);
                });

                // Prepare reportData list from the supplier data map
                List<Map<String, Object>> purchaseHistoryData = new ArrayList<>(supplierDataMap.values());

                // Prepare column details
                column = new LinkedHashMap<>();
                column.put("supplier_id", "SUPPLIER ID");
                column.put("supplier_name", "SUPPLIER NAME");
                column.put("purchase_id", "PURCHASE ID");
                column.put("purchase_date", "PURCHASE DATE");
                column.put("total_amount", "TOTAL AMOUNT");
                column.put("grn_item_id", "GRN ITEM ID");
                column.put("grn_date", "GRN DATE");
                column.put("grn_quantity", "GRN QUANTITY");
                column.put("grn_total_price", "GRN TOTAL PRICE");
                column.put("product_id", "PRODUCT ID");
                column.put("product_name", "PRODUCT NAME");
                column.put("category_id", "CATEGORY ID");
                column.put("category_name", "CATEGORY NAME");
                column.put("brand_id", "BRAND ID");
                column.put("brand_name", "BRAND NAME");

                // Prepare summary details for purchase history
                double totalRevenue = purchaseHistoryData.stream()
                        .flatMap(data -> ((List<Map<String, Object>>) data.get("purchases")).stream())
                        .flatMap(purchaseEntry -> {
                            // Access the list of GRNs from each purchase
                            List<Map<String, Object>> grns = (List<Map<String, Object>>) purchaseEntry.get("grns");
                            return grns.stream(); // Flatten the list of GRN entries
                        }).mapToDouble(grnEntry -> {
                            // Get the total_price from each GRN item
                            Double totalPrice = MoneyUtils.truncateToTwoDecimals((Double) grnEntry.get("grn_total_price"));
                            return totalPrice != null ? totalPrice.doubleValue() : 0.0; // Use 0.0 if total_price is null
                        }).sum();

                // Summary details
                summary = new LinkedHashMap<>();
                summary.put("totalRevenue", MoneyUtils.truncateToTwoDecimals(totalRevenue));

                // Prepare response data
                purchaseData = new LinkedHashMap<>();
                purchaseData.put("module", "supplier");
                purchaseData.put("reportType", reportType);
                purchaseData.put("startDate", String.valueOf(fromDate));
                purchaseData.put("endDate", String.valueOf(toDate));
                purchaseData.put("reportData", purchaseHistoryData);
                purchaseData.put("column", column);
                purchaseData.put("summary", summary);

                // Create and return the structured ApiResponseStructure for purchase history
                return new ApiResponseStructure<>("Success", 200, "Data retrieved.", purchaseData);

            default:
                createLogger.createLogger("error", path, "POST", "Invalid report type provided.", "");
                return new ApiResponseStructure<>("Error", 400, "Invalid report type provided.", new HashMap<>());
        }
    }

}