package io.netfoundry.billing.external.netsuite.process;

import static io.netfoundry.common.util.jdbc.sql.FlatWhere.all;
import java.time.Instant;
import java.time.LocalDate;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.springframework.util.CollectionUtils;
import io.netfoundry.billing.config.UsageConfig;
import io.netfoundry.billing.domain.contract.Contract;
import io.netfoundry.billing.domain.contract.ContractService;
import io.netfoundry.billing.domain.subscription.SubscriptionCacheRepository;
import io.netfoundry.billing.domain.usage.tracking.DailyUsage;
import io.netfoundry.billing.domain.usage.tracking.DailyUsageRepository;
import io.netfoundry.billing.domain.usage.tracking.UsageTrackingService;
import io.netfoundry.billing.external.netsuite.client.NetSuiteApiClient;
import io.netfoundry.billing.external.netsuite.domain.LineItemsItem;
import io.netfoundry.billing.external.netsuite.domain.NetSuiteSubscription;
import io.netfoundry.billing.external.netsuite.domain.NetSuiteSusbscriptionUsage;
import io.netfoundry.billing.external.netsuite.domain.Subscription;
import io.netfoundry.billing.external.netsuite.domain.UsageSubscription;
import io.netfoundry.billing.external.netsuite.domain.UsageSubscriptionLine;
import io.netfoundry.common.util.jdbc.sql.Operators;
import io.netfoundry.processing.ProcessResult;
import io.netfoundry.processing.stereotypes.CollectionArg;
import io.netfoundry.processing.stereotypes.ExceptionHandler;
import io.netfoundry.processing.stereotypes.Next;
import io.netfoundry.processing.stereotypes.Process;
import io.netfoundry.processing.stereotypes.Step;
import lombok.Builder;
import lombok.extern.jackson.Jacksonized;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Process("NetSuite Subscription Usage Process")
public class NetSuiteSubscriptionUsageSubmitProcess {

  private static final String COLUMN_POSTING_DATE = "postingDate";
  private static final String COLUMN_CONTRACT_ID = "contractId";
  private static final String COMPONENT_DATA = "dataGb";
  private static final String COMPONENT_ENDPOINT = "endpoint";
  private static final String COMPONENT_NETWORK = "network";
  private static final String COMPONENT_PRIVATEGATEWAY = "privateGateway";


  private final UsageTrackingService usageTrackingService;
  private final NetSuiteApiClient netSuiteApiClient;
  private final UsageConfig usageConfig;
  private final SubscriptionCacheRepository subscriptionCacheRepository;
  private final DailyUsageRepository dailyUsageRepository;


  public NetSuiteSubscriptionUsageSubmitProcess(ContractService contractService, UsageTrackingService usageTrackingService, SubscriptionCacheRepository subscriptionCacheRepository,
      NetSuiteApiClient apiClient, UsageConfig usageConfig, DailyUsageRepository dailyUsageRepository) {
    this.usageTrackingService = usageTrackingService;
    this.subscriptionCacheRepository = subscriptionCacheRepository;
    this.netSuiteApiClient = apiClient;
    this.usageConfig = usageConfig;
    this.dailyUsageRepository = dailyUsageRepository;
  }

  @Step("Get All Active Contracts to Submit Usage Record")
  @Next("createUsageRecords")
  public ProcessResult<Void> processDailyComponentUsage(NetSuiteSubscriptionUsageSubmitProcessContext netSuiteSubscriptionUsageProcessContext) {
    List<Contract> contractList = netSuiteSubscriptionUsageProcessContext.getContracts().stream().filter(contract -> {
      Set<DailyUsage> dailyUsageList = usageTrackingService.find(contract.getId());
      return !CollectionUtils.isEmpty(dailyUsageList);
    }).collect(Collectors.toList());

    if (CollectionUtils.isEmpty(contractList)) {
      log.warn("No daily usage found for the contracts");
      return ProcessResult.fail();
    }
    netSuiteSubscriptionUsageProcessContext.setContracts(contractList);
    return ProcessResult.success();
  }


  @Step("Process Usage Record for All Contracts")
  @Next("createUsageRecord")
  private ProcessResult<Contract> createUsageRecords(NetSuiteSubscriptionUsageSubmitProcessContext netSuiteSubscriptionUsageProcessContext) {
    return ProcessResult.parallel(netSuiteSubscriptionUsageProcessContext.getContracts()).build();

  }

  @Step("Create Usage Record in NetSuite")
  @Next("finish")
  private ProcessResult<Boolean> createUsageRecord(@CollectionArg Contract contract, NetSuiteSubscriptionUsageSubmitProcessContext netSuiteSubscriptionUsageProcessContext) {
    netSuiteSubscriptionUsageProcessContext.setAccountId(contract.getAccountId());
    netSuiteSubscriptionUsageProcessContext.setContractId(contract.getId());
    Optional<NetSuiteSubscription> subscriptionforSuite = subscriptionCacheRepository.getSubscription(contract.getProductId());
    subscriptionforSuite.get().getSubscription().getLineItems();
    Subscription subscription = subscriptionforSuite.get().getSubscription();
    Set<DailyUsage> dailyUsages = dailyUsageRepository
        .find(all(COLUMN_CONTRACT_ID, contract.getId(), COLUMN_POSTING_DATE, Operators.gteq(Instant.ofEpochMilli(netSuiteSubscriptionUsageProcessContext.getFromTimeEpochMs())),
            COLUMN_POSTING_DATE, Operators.lteq(Instant.ofEpochMilli(netSuiteSubscriptionUsageProcessContext.getToTimeEpochMs()))));
    Map<String, Set<DailyUsage>> dailyUsageGroupByMeteredItemMap = dailyUsages.stream().collect(Collectors.groupingBy(DailyUsage::getMeteredItem, Collectors.toSet()));
    //postComponentUsageData(getOccurenceMap(dailyUsageGroupByMeteredItemMap.getOrDefault(COMPONENT_ENDPOINT, Collections.emptySet()), subscription), subscription);
    //postComponentUsageData(getOccurenceMap(dailyUsageGroupByMeteredItemMap.getOrDefault(COMPONENT_NETWORK, Collections.emptySet()), subscription), subscription);
    //postComponentUsageData(getOccurenceMap(dailyUsageGroupByMeteredItemMap.getOrDefault(COMPONENT_PRIVATEGATEWAY, Collections.emptySet()), subscription),
    //    subscription);
    Map<String, Double> aggregatedDataMap =
        dailyUsageGroupByMeteredItemMap.entrySet().stream().filter(entry -> COMPONENT_DATA.equals(entry.getKey())).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))
            .entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().stream().mapToDouble(DailyUsage::getMeteredQuantity).sum()));
    //postDataUsage(aggregatedDataMap, subscription);
    return ProcessResult.success();
  }

  private void postComponentUsageData(Map<String, Map<Double, Long>> dailyUsageMap, Subscription subscription) {
    dailyUsageMap.forEach((meteredItem, meteredItemQuantityMap) -> {
      NetSuiteSusbscriptionUsage netSuiteSusbscriptionUsage = new NetSuiteSusbscriptionUsage();
      UsageSubscription usageSubscription = new UsageSubscription();
      UsageSubscriptionLine usageSubscriptionLine = new UsageSubscriptionLine();
      List<String> meteredItems = usageConfig.getItemMap().get(meteredItem);
      Optional<LineItemsItem> lineItem = subscription.getLineItems().stream()
          .filter(itemsItem -> meteredItems.contains(itemsItem.getDisplayName())).findFirst();
      int minUsageDays = lineItem.map(LineItemsItem::getMinUsageDays).map(String::trim).filter(days -> days.matches("^[0-9]+$")).map(Integer::parseInt).orElse(0);
      Double meteredItemQuantity =
          meteredItemQuantityMap.entrySet().stream().filter(e -> e.getValue() >= minUsageDays).max(Map.Entry.comparingByKey()).map(Map.Entry::getKey).orElse(0.0);
      netSuiteSusbscriptionUsage.setUsageQuantity(meteredItemQuantity.intValue());
      netSuiteSusbscriptionUsage.setUsageDate(LocalDate.now().toString());
      usageSubscription.setId(String.valueOf(subscription.getId()));
      netSuiteSusbscriptionUsage.setUsageSubscription(usageSubscription);
      usageSubscriptionLine.setId(String.valueOf(lineItem.get().getLineId()));
      netSuiteSusbscriptionUsage.setUsageSubscriptionLine(usageSubscriptionLine);
      netSuiteApiClient.submitSubscriptionUsage(netSuiteSusbscriptionUsage);
    });

  }

  private void postDataUsage(Map<String, Double> aggregatedMap, Subscription subscription) {
    aggregatedMap.forEach((meteredItem, meteredItemQuantity) -> {
      NetSuiteSusbscriptionUsage netSuiteSusbscriptionUsage = new NetSuiteSusbscriptionUsage();
      UsageSubscription usageSubscription = new UsageSubscription();
      UsageSubscriptionLine usageSubscriptionLine = new UsageSubscriptionLine();
      List<String> meteredItems = usageConfig.getItemMap().get(meteredItem);
      Optional<LineItemsItem> lineItem = subscription.getLineItems().stream().filter(itemsItem -> meteredItems.contains(itemsItem.getDisplayName())).findFirst();
      netSuiteSusbscriptionUsage.setUsageQuantity(meteredItemQuantity.intValue());
      netSuiteSusbscriptionUsage.setUsageDate(LocalDate.now().toString());
      usageSubscription.setId(String.valueOf(subscription.getId()));
      netSuiteSusbscriptionUsage.setUsageSubscription(usageSubscription);
      usageSubscriptionLine.setId(String.valueOf(lineItem.get().getLineId()));
      netSuiteSusbscriptionUsage.setUsageSubscriptionLine(usageSubscriptionLine);
      netSuiteApiClient.submitSubscriptionUsage(netSuiteSusbscriptionUsage);
    });

  }

  private Map<String, Map<Double, Long>> getOccurenceMap(Set<DailyUsage> dailyUsages, Subscription subscription) {
    Map<String, Map<Double, Long>> occurrenceMapPerItem =
        dailyUsages.stream().collect(Collectors.groupingBy(DailyUsage::getMeteredItem, Collectors.groupingBy(DailyUsage::getMeteredQuantity, Collectors.counting())));
    log.info("Occurence Map : {} for Component:{}", occurrenceMapPerItem);
    return occurrenceMapPerItem;
  }

  @Step("Finish")
  private ProcessResult<Void> finish(NetSuiteSubscriptionUsageSubmitProcessContext netSuiteSubscriptionUsageProcessContext) {
    log.info("Monthly NetSuite Usage Submission finished successfully for the accountId:{} and contractId:{}", netSuiteSubscriptionUsageProcessContext.getAccountId(),
        netSuiteSubscriptionUsageProcessContext.getContractId());
    return ProcessResult.success();
  }

  @ExceptionHandler
  private void handleException(Exception e, NetSuiteSubscriptionUsageSubmitProcessContext netSuiteSubscriptionUsageProcessContext) {
    log.warn("Monthly NetSuite Usage Submission failed for the accountId:{} and contractId:{}", netSuiteSubscriptionUsageProcessContext.getAccountId(),
        netSuiteSubscriptionUsageProcessContext.getContractId(), e);

  }

  @lombok.Data
  @Builder
  @Jacksonized
  public static class NetSuiteSubscriptionUsageSubmitProcessContext {
    UUID accountId;
    UUID contractId;
    List<Contract> contracts;
    List<DailyUsage> dailyUsages;
    Long fromTimeEpochMs;
    Long toTimeEpochMs;
  }
}
