package io.netfoundry.billing.controller;

import java.time.Instant;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import io.netfoundry.billing.config.BannerSchedulingConfiguration;
import io.netfoundry.billing.domain.contract.Contract;
import io.netfoundry.billing.domain.contract.ContractService;
import io.netfoundry.billing.external.netsuite.client.NetSuiteApiClient;
import io.netfoundry.billing.process.EnterpriseOverageInvoiceProcess;
import io.netfoundry.billing.process.EnterpriseOverageInvoiceProcess.EnterpriseOverageInvoiceContext;
import io.netfoundry.billing.processor.consistency.BillingConsistencyCheckProcess;
import io.netfoundry.billing.processor.usage.EnterpriseOverageFinalInvoicingProcess;
import io.netfoundry.billing.processor.usage.EnterpriseOverageInvoicingProcess;
import io.netfoundry.billing.processor.usage.EnterpriseUsageMeteringService;
import io.netfoundry.billing.service.AuthUtilService;
import jakarta.validation.constraints.NotNull;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;


/**
 * NOTE: This is a controller to manually trigger the overage tracking, invoicing and reporting process in case of failure through regular scheduled job
 */
@Slf4j
@RestController
public class AdhocJobController {
  
    private final static String FIRST_DAY_OF_MONTH = "First day of month in epoch : {}";
    private final static String LAST_DAY_OF_MONTH = "last day of month in epoch : {}";
    private final static String ACTIVE_CONTRACTS = "Finding active contracts for plan : {} in paged mode : [ pageNo: {}, pageSize:{} ]";
    private final static String ACTIVE_CONTRACT_NOT_FOUND = "Active contracts not found for the plan : {}";
    private final static String REPORTING_ERROR="Error executing overage invoice and reporting process";

    private final BillingConsistencyCheckProcess billingConsistencyCheckProcess;

    private final EnterpriseOverageInvoicingProcess enterpriseOverageInvoicingProcess;

    private final EnterpriseOverageFinalInvoicingProcess enterpriseOverageFinalInvoicingProcess;

    private final EnterpriseUsageMeteringService enterpriseUsageMeteringService;

    private final ContractService contractService;

    private final AuthUtilService authUtilService;

    private final BannerSchedulingConfiguration bannerSchedulingConfiguration;

    private final EnterpriseOverageInvoiceProcess overageInvoiceProcess;

    private final NetSuiteApiClient netSuiteApiClient;


    @Autowired
    public AdhocJobController(ContractService contractService,
        EnterpriseOverageInvoicingProcess enterpriseOverageInvoicingProcess,
        EnterpriseOverageFinalInvoicingProcess enterpriseOverageFinalInvoicingProcess,
        EnterpriseUsageMeteringService enterpriseUsageMeteringService,
        BillingConsistencyCheckProcess billingConsistencyCheckProcess,
        AuthUtilService authUtilService,NetSuiteApiClient netSuiteApiClient,
        BannerSchedulingConfiguration bannerSchedulingConfiguration, io.netfoundry.billing.process.EnterpriseOverageInvoiceProcess overageInvoicingProcess) {
        this.contractService = contractService;
        this.enterpriseOverageInvoicingProcess = enterpriseOverageInvoicingProcess;
        this.enterpriseOverageFinalInvoicingProcess = enterpriseOverageFinalInvoicingProcess;
        this.enterpriseUsageMeteringService = enterpriseUsageMeteringService;
        this.billingConsistencyCheckProcess = billingConsistencyCheckProcess;
        this.authUtilService = authUtilService;
        this.bannerSchedulingConfiguration = bannerSchedulingConfiguration;
        this.overageInvoiceProcess = overageInvoicingProcess;
        this.netSuiteApiClient = netSuiteApiClient;
      }

    @PostMapping(value = "/enterprise-overage-invoices", produces = MediaType.APPLICATION_JSON_VALUE)
    public void submitOverageForInvoicing(@NotNull @RequestParam("planId") String planId,
        @NotNull @RequestParam("lastDayOfMonthEpochMs") Long lastDayOfMonthEpochMs,
        @RequestParam(value = "contractId", required = false)
        UUID contractId, @RequestParam(value = "retry", required = false) boolean retry) {

        if (ENTERPRISE_PLANS.getByPlanId(planId) == null)
            throw new IllegalArgumentException(
                String.format("%s is not a valid enterprise plan id", planId));


        authUtilService.validateEnterpriseSubscriptionReadAll();

        LocalDateTime lastDayOfMonth =
            LocalDateTime.ofInstant(Instant.ofEpochMilli(lastDayOfMonthEpochMs),
                ZoneId.of("UTC"));

        Long firstDayOfMonthEpoch =
            lastDayOfMonth.withDayOfMonth(1).with(LocalTime.MIN).toInstant(ZoneOffset.UTC)
                .toEpochMilli();
        Long lastDayOfMonthEpoch =
            lastDayOfMonth.with(LocalTime.MAX).toInstant(ZoneOffset.UTC).toEpochMilli();

        log.info(FIRST_DAY_OF_MONTH, firstDayOfMonthEpoch);
        log.info(LAST_DAY_OF_MONTH, lastDayOfMonthEpoch);


        Page<Contract> contracts = null;
        int page = 0;
        int size = 5;
        do {

            Set<Contract> contractsToProcess = new HashSet<>();

            if (contractId != null)
                contractService.getContractById(contractId).ifPresent(contractsToProcess::add);
            else {
                log.info(
                    ACTIVE_CONTRACTS,
                    planId, page, size);

                contracts = contractService.listContracts(planId, false,
                    PageRequest.of(page, size));

                if (contracts != null && !CollectionUtils.isEmpty(contracts.getContent()))
                    contractsToProcess.addAll(contracts.getContent());
                else
                    return;
            }

            if (CollectionUtils.isEmpty(contractsToProcess)) {
                log.warn(ACTIVE_CONTRACT_NOT_FOUND, planId);
                return;
            }

            contractsToProcess.forEach(contract -> {

                Map<String, String> params = new HashMap<>();
                params.put("billingAccountId", contract.getAccountId().toString());
                params.put("contractId", contract.getId().toString());
                params.put("fromTimeEpochMs", String.valueOf(firstDayOfMonthEpoch));
                params.put("toTimeEpochMs", String.valueOf(lastDayOfMonthEpoch));

                try {
                    Map<String, String> executionResult =
                        enterpriseOverageInvoicingProcess.createAndExecute(params, retry);
                    log.info(
                        "Overage invoicing & reporting process execution result for contract id : {} - {}",
                        contract.getId(),
                        executionResult);
                } catch (Exception e) {
                    log.error(REPORTING_ERROR, e);
                }
            });

            page++;
        } while (contracts != null && !contracts.isLast());

    }

    @PostMapping(value = "/create-enterprise-overage-invoices", produces = MediaType.APPLICATION_JSON_VALUE)
    public void createInvoiceForOverge(@NotNull @RequestParam("planId") String planId, @NotNull @RequestParam("lastDayOfMonthEpochMs") Long lastDayOfMonthEpochMs,
        @RequestParam(value = "contractId", required = false) UUID contractId) {

      if (ENTERPRISE_PLANS.getByPlanId(planId) == null)
        throw new IllegalArgumentException(String.format("%s is not a valid enterprise plan id", planId));
      authUtilService.validateEnterpriseSubscriptionReadAll();
      LocalDateTime lastDayOfMonth = LocalDateTime.ofInstant(Instant.ofEpochMilli(lastDayOfMonthEpochMs), ZoneId.of("UTC"));
      Long firstDayOfMonthEpoch = lastDayOfMonth.withDayOfMonth(1).with(LocalTime.MIN).toInstant(ZoneOffset.UTC).toEpochMilli();
      Long lastDayOfMonthEpoch = lastDayOfMonth.with(LocalTime.MAX).toInstant(ZoneOffset.UTC).toEpochMilli();
      LocalDateTime firstDayOfMonthDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(firstDayOfMonthEpoch), ZoneOffset.UTC);
      LocalDateTime lastDayOfMonthDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(lastDayOfMonthEpoch), ZoneOffset.UTC);
      log.info(FIRST_DAY_OF_MONTH, firstDayOfMonthDateTime);
      log.info(LAST_DAY_OF_MONTH, lastDayOfMonthDateTime);
      Page<Contract> contracts = null;
      int page = 0;
      int size = 5;
      do {

        List<Contract> contractsToProcess = new ArrayList<>();
        if (contractId != null)
          contractService.getContractById(contractId).ifPresent(contractsToProcess::add);
        else {
          log.info(ACTIVE_CONTRACTS, planId, page, size);
          contracts = contractService.listContracts(planId, false, PageRequest.of(page, size));
          if (contracts != null && !CollectionUtils.isEmpty(contracts.getContent()))
            contractsToProcess.addAll(contracts.getContent());
          else
            return;
        }
        if (CollectionUtils.isEmpty(contractsToProcess)) {
          log.warn(ACTIVE_CONTRACT_NOT_FOUND, planId);
          return;
        }
        try {
          EnterpriseOverageInvoiceContext enterpriseOverageInvoiceContext =
              EnterpriseOverageInvoiceContext.builder().contracts(contractsToProcess).fromTimeEpochMs(firstDayOfMonthEpoch).toTimeEpochMs(lastDayOfMonthEpoch)
                  .invoiceType("INVOICING").build();
          overageInvoiceProcess.createOverageInvoice(enterpriseOverageInvoiceContext);
        } catch (Exception e) {
          log.error(REPORTING_ERROR, e);
        }
        page++;
      } while (contracts != null && !contracts.isLast());

    }


    @PostMapping(value = "/enterprise-overage-final-invoicing", produces = MediaType.APPLICATION_JSON_VALUE)
    public void submitOverageForFinalInvoicing(@NotNull @RequestParam("planId") String planId,
        @NotNull @RequestParam("lastDayOfMonthEpochMs") Long lastDayOfMonthEpochMs,
        @RequestParam(value = "contractId", required = false)
        UUID contractId, @RequestParam(value = "retry", required = false) boolean retry) {

        if (ENTERPRISE_PLANS.getByPlanId(planId) == null)
            throw new IllegalArgumentException(
                String.format("%s is not a valid enterprise plan id", planId));

        authUtilService.validateEnterpriseSubscriptionReadAll();

        LocalDateTime lastDayOfMonth =
            LocalDateTime.ofInstant(Instant.ofEpochMilli(lastDayOfMonthEpochMs),
                ZoneId.of("UTC"));

        Long firstDayOfMonthEpoch =
            lastDayOfMonth.withDayOfMonth(1).with(LocalTime.MIN).toInstant(ZoneOffset.UTC)
                .toEpochMilli();

        Long lastDayOfMonthEpoch =
            lastDayOfMonth.with(LocalTime.MAX).toInstant(ZoneOffset.UTC).toEpochMilli();

        log.info(FIRST_DAY_OF_MONTH, firstDayOfMonthEpoch);
        log.info(LAST_DAY_OF_MONTH, lastDayOfMonthEpoch);

        long firstDayHourMinOfNextMonth =
            lastDayOfMonth.plusDays(1).with(LocalTime.MIN).toInstant(ZoneOffset.UTC)
                .toEpochMilli();

        long firstDayHourMaxOfNextMonth =
            lastDayOfMonth.plusDays(1).with(LocalTime.MAX).toInstant(ZoneOffset.UTC)
                .toEpochMilli();

        log.info(
            "Tracking overages for - [Plan id : {}] cancelled between [From time : {}] [To time :{}]",
            planId, Instant.ofEpochMilli(firstDayHourMinOfNextMonth),
            Instant.ofEpochMilli(firstDayHourMaxOfNextMonth));

        Page<Contract> contracts;
        int page = 0;
        int size = 5;
        do {

            log.info(
                "Finding cancelled contracts for plan : {} in paged mode : [ pageNo: {}, pageSize:{} ]",
                planId, page, size);

            contracts = contractService.listCancelledContracts(planId,
                firstDayHourMinOfNextMonth, firstDayHourMaxOfNextMonth, PageRequest.of(page, size));

            if (contracts == null || CollectionUtils.isEmpty(contracts.getContent())) {
                log.warn("Cancelled contracts not found for the plan : {} between {} and {}",
                    planId, Instant.ofEpochMilli(firstDayHourMinOfNextMonth),
                    Instant.ofEpochMilli(firstDayHourMaxOfNextMonth));
                return;
            }

            contracts.forEach(contract -> {

                Map<String, String> params = new HashMap<>();
                params.put("billingAccountId", contract.getAccountId().toString());
                params.put("contractId", contract.getId().toString());
                params.put("fromTimeEpochMs", String.valueOf(firstDayOfMonthEpoch));
                params.put("toTimeEpochMs", String.valueOf(lastDayOfMonthEpoch));

                try {
                    Map<String, String> executionResult =
                        enterpriseOverageFinalInvoicingProcess.createAndExecute(params, retry);
                    log.info(
                        "Overage final invoicing & reporting process execution result for contract id : {} - {}",
                        contract.getId(),
                        executionResult);
                } catch (Exception e) {
                    log.error("Error executing overage final invoice and reporting process", e);
                }
            });

            page++;
        } while (!contracts.isLast());
    }

    @PostMapping(value = "/create-enterprise-overage-final-invoicing")
    public void createOverageForFinalInvoicing(@NotNull @RequestParam("planId") String planId, @NotNull @RequestParam("lastDayOfMonthEpochMs") Long lastDayOfMonthEpochMs,
        @RequestParam(value = "contractId", required = false) UUID contractId) {

      if (ENTERPRISE_PLANS.getByPlanId(planId) == null) throw new IllegalArgumentException(String.format("%s is not a valid enterprise plan id", planId));
      authUtilService.validateEnterpriseSubscriptionReadAll();
      LocalDateTime lastDayOfMonth = LocalDateTime.ofInstant(Instant.ofEpochMilli(lastDayOfMonthEpochMs), ZoneId.of("UTC"));
      Long firstDayOfMonthEpoch = lastDayOfMonth.withDayOfMonth(1).with(LocalTime.MIN).toInstant(ZoneOffset.UTC).toEpochMilli();
      Long lastDayOfMonthEpoch = lastDayOfMonth.with(LocalTime.MAX).toInstant(ZoneOffset.UTC).toEpochMilli();
      log.info(FIRST_DAY_OF_MONTH, firstDayOfMonthEpoch);
      log.info(LAST_DAY_OF_MONTH, lastDayOfMonthEpoch);
      long firstDayHourMinOfNextMonth = lastDayOfMonth.plusDays(1).with(LocalTime.MIN).toInstant(ZoneOffset.UTC).toEpochMilli();
      long firstDayHourMaxOfNextMonth = lastDayOfMonth.plusDays(1).with(LocalTime.MAX).toInstant(ZoneOffset.UTC).toEpochMilli();
      log.info("Tracking overages for - [Plan id : {}] cancelled between [From time : {}] [To time :{}]", planId, Instant.ofEpochMilli(firstDayHourMinOfNextMonth),
          Instant.ofEpochMilli(firstDayHourMaxOfNextMonth));
      Page<Contract> contracts;
      int page = 0;
      int size = 5;
      do {

        log.info("Finding cancelled contracts for plan : {} in paged mode : [ pageNo: {}, pageSize:{} ]", planId, page, size);
        contracts = contractService.listCancelledContracts(planId, firstDayHourMinOfNextMonth, firstDayHourMaxOfNextMonth, PageRequest.of(page, size));
        if (contracts == null || CollectionUtils.isEmpty(contracts.getContent())) {
          log.warn("Cancelled contracts not found for the plan : {} between {} and {}", planId, Instant.ofEpochMilli(firstDayHourMinOfNextMonth),
              Instant.ofEpochMilli(firstDayHourMaxOfNextMonth));
          return;
        }
        try {
          EnterpriseOverageInvoiceContext enterpriseOverageInvoiceContext =
              EnterpriseOverageInvoiceContext.builder().contracts(contracts.getContent()).fromTimeEpochMs(firstDayOfMonthEpoch).toTimeEpochMs(lastDayOfMonthEpoch)
                  .invoiceType("FINAL_INVOICING").build();
          overageInvoiceProcess.createOverageInvoice(enterpriseOverageInvoiceContext);
        } catch (Exception e) {
          log.error(REPORTING_ERROR, e);
        }
        page++;
      } while (!contracts.isLast());
    }

    @PostMapping(value = "/enterprise-overage-metering", produces = MediaType.APPLICATION_JSON_VALUE)
    public void createMeteredOverages(@NotNull @RequestParam("planId") String planId,
        @NotNull @RequestParam("minTimeOfDayEpochMs") Long minTimeOfDayEpochMs,
        @RequestParam(value = "contractId", required = false)
        UUID contractId) {

        if (ENTERPRISE_PLANS.getByPlanId(planId) == null)
            throw new IllegalArgumentException(
                String.format("%s is not a valid enterprise plan id", planId));

        authUtilService.validateEnterpriseSubscriptionReadAll();

        LocalDateTime localDate =
            LocalDateTime.ofInstant(Instant.ofEpochMilli(minTimeOfDayEpochMs), ZoneId.of("UTC"));

        Page<Contract> contracts = null;
        int page = 0;
        int size = 5;
        do {
            List<Contract> contractsToProcess = new ArrayList<>();

            if (contractId != null)
                contractService.getContractById(contractId).ifPresent(contractsToProcess::add);
            else {
                log.info(ACTIVE_CONTRACTS, planId, page, size);

                contracts = contractService.listContracts(planId, false,
                    PageRequest.of(page, size));

                if (contracts != null && !CollectionUtils.isEmpty(contracts.getContent()))
                    contractsToProcess.addAll(contracts.getContent());
                else
                    return;
            }

            if (CollectionUtils.isEmpty(contractsToProcess)) {
                log.warn(ACTIVE_CONTRACT_NOT_FOUND, planId);
                return;
            }

            long fromTimeEpochMs = localDate.with(LocalTime.MIN).toInstant(ZoneOffset.UTC)
                .toEpochMilli();

            long toTimeEpochMs =
                localDate.plusDays(1).with(LocalTime.MIN).toInstant(ZoneOffset.UTC)
                    .toEpochMilli();

            log.info(
                "Posting metered overages for plan : {} from {} to {}",
                planId, Instant.ofEpochMilli(fromTimeEpochMs),
                Instant.ofEpochMilli(toTimeEpochMs));

            enterpriseUsageMeteringService.postEnterpriseSubscriptionOverage(
                planId, fromTimeEpochMs, toTimeEpochMs,
                contracts != null ? contracts.getContent() : contractsToProcess);

            page++;
        } while (contracts != null && !contracts.isLast());


    }

    @PostMapping(value = "/overdue-invoice-banners", produces = MediaType.APPLICATION_JSON_VALUE)
    public void createBannerForOverdueInvoice(@NotNull @RequestParam("planId") String planId,
        @NotNull @RequestParam("fromTimeEpochMs") Long fromTimeEpochMs,
        @NotNull @RequestParam("toTimeEpochMs") Long toTimeEpochMs) {

        if (planId.equals("Netfoundry-Enterprise"))
            authUtilService.validateEnterpriseSubscriptionReadAll();
        else
            throw new IllegalArgumentException(
                "Unsupported plan for invoice due banners: " + planId);

        bannerSchedulingConfiguration.findInvoicesDueAndCreateBanner(fromTimeEpochMs, toTimeEpochMs,
            planId);
    }

    @PostMapping(value = "/expired-contract-banners", produces = MediaType.APPLICATION_JSON_VALUE)
    public void createBannerForExpiredContract() {

        authUtilService.validateAccountReadAll();

        bannerSchedulingConfiguration.findAccountsWithoutActiveContracts();
    }

    @PostMapping(value = "/billing-consistency-check", produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<Void> checkBillingAccountConsistency() {

        authUtilService.validateAccountReadAll();

        billingConsistencyCheckProcess.execute(Instant.now());

        return ResponseEntity.accepted().build();
    }

    @Getter private enum ENTERPRISE_PLANS {
        ENTERPRISE("Netfoundry-Enterprise");
        private final String planId;

        ENTERPRISE_PLANS(String planId) {
            this.planId = planId;
        }

        public static ENTERPRISE_PLANS getByPlanId(String planId) {
            return Arrays.stream(ENTERPRISE_PLANS.values())
                .filter(enterprisePlan -> enterprisePlan.getPlanId().equals(planId)).findFirst()
                .orElse(null);
        }

    }

    @GetMapping(value = "/getSubscription")
    public ResponseEntity<String> getSubscription(String subscriptionId) {
      String susbcription = netSuiteApiClient.getSusbcription(subscriptionId);
      ResponseEntity.ok(susbcription);
      return ResponseEntity.ok(susbcription);
    }

    @GetMapping(value = "/getSubscription/{id}")
    public ResponseEntity<String> getSubscriptionById(@PathVariable String id) {
      String susbcription = netSuiteApiClient.getSusbcription(id);
      ResponseEntity.ok(susbcription);
      return ResponseEntity.ok(susbcription);
    }
  }

