package io.netfoundry.core.process;

import java.time.Duration;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.retry.support.RetryTemplate;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.netfoundry.core.domain.Status;
import io.netfoundry.core.domain.config.type.ConfigTypeImport;
import io.netfoundry.core.domain.host.Host;
import io.netfoundry.core.domain.host.HostService;
import io.netfoundry.core.domain.network.Network;
import io.netfoundry.core.domain.network.NetworkService;
import io.netfoundry.core.domain.networkcontroller.FindNetworkController;
import io.netfoundry.core.domain.networkcontroller.NetworkController;
import io.netfoundry.core.domain.networkcontroller.NetworkControllerService;
import io.netfoundry.core.event.ComponentType;
import io.netfoundry.core.event.EventRecord;
import io.netfoundry.core.event.EventService;
import io.netfoundry.core.event.EventSeverity;
import io.netfoundry.core.event.EventType;
import io.netfoundry.core.event.MetricType;
import io.netfoundry.core.external.dns.DomainNameService;
import io.netfoundry.core.external.sds.SDSService;
import io.netfoundry.processing.ProcessResult;
import io.netfoundry.processing.stereotypes.ExceptionHandler;
import io.netfoundry.processing.stereotypes.Metadata;
import io.netfoundry.processing.stereotypes.Next;
import io.netfoundry.processing.stereotypes.Process;
import io.netfoundry.processing.stereotypes.Step;
import lombok.Data;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Process("Install Public Certificate")
public class InstallPublicCertificateProcess {

  private final EventService eventService;
  private final NetworkService networkService;
  private final NetworkControllerService networkControllerService;
  private final SDSService sdsService;
  private final HostService hostService;
  private final DomainNameService domainNameService;
  private final ConfigTypeImport configTypeImport;
  private final RetryTemplate retryTemplate;
  private final Duration networkControllerInstallTimeout;

  @Autowired
  public InstallPublicCertificateProcess(
      EventService eventService,
      NetworkService networkService,
      NetworkControllerService networkControllerService,
      SDSService sdsService,
      DomainNameService domainNameService,
      ConfigTypeImport configTypeImport,
      RetryTemplate retryTemplate,
      HostService hostService,
      @Value("${io.netfoundry.core.timeout.networkControllerInstall:PT10M}") Duration networkControllerInstallTimeout) {
    this.eventService = eventService;
    this.networkService = networkService;
    this.networkControllerService = networkControllerService;
    this.sdsService = sdsService;
    this.configTypeImport = configTypeImport;
    this.domainNameService = domainNameService;
    this.hostService = hostService;
    this.retryTemplate = retryTemplate;
    this.networkControllerInstallTimeout = networkControllerInstallTimeout;
  }

  @Step(name="Update Network Public Certificate status", updatesMetadata = true)
  @Next("registerDomain")
  public ProcessResult<Network> installPublicCertificate(Context context) {
    Network network = networkService.updateNetworkPublicCert(context.getNetworkId(), context.getPublicCertEnabled());
    return ProcessResult.value(network).success();
  }

  @Step("Register Domain Name")
  @Next("reconfigureZitiController")
  private boolean registerDomain(Context context) {
    Set<NetworkController> networkControllerSet = networkControllerService.find(FindNetworkController.by().networkId(context.getNetworkId()).build());
    final NetworkController networkController = networkControllerSet.stream().findFirst().get();
    context.networkControllerId = networkController.getId();
    Host host = hostService.getHost(networkController.getHostId());
    if (context.getPublicCertEnabled())
      this.domainNameService.upsertSecondaryDomain(networkController.getId(), host.getIpAddress());
    return true;
  }


  @Step("Re-Install Network Controller software")
  @Next("readZitiFixtures")
  private boolean reconfigureZitiController(Context context) {

    return retryTemplate.execute(c -> {
      final NetworkController networkController = networkControllerService.getNetworkController(context.getNetworkControllerId());
      sdsService.installNetworkController(networkController.getId(), networkController.getHostId(),
          Optional.ofNullable(networkControllerInstallTimeout).map(Duration::toMillis).orElse(null));
      networkControllerService.updateNetworkControllerStatus(networkController, Status.PROVISIONED);
      return true;
    }, c -> {
      throw new IllegalArgumentException("Unable to install Ziti network controller", c.getLastThrowable());
    });
  }

  @Step("Read Ziti fixtures")
  @Next("finish")
  private void readZitiFixtures(Context context) {

    configTypeImport.importMissing(context.getNetworkId());

  }


  @Step("Finish")
  private void finish(Context context) {

    log.info("Launched and created Network: {}", context.getNetworkId());
    Network network = networkService.updateNetworkStatus(context.getNetworkId(), Status.PROVISIONED);
    eventService.publishEvent(
        new EventRecord(MetricType.MOP_EVENT, network.getNetworkGroupId(), network.getId(), ComponentType.NETWORK, network.getId().toString(),
            network.getName(), EventType.ACTIVE, EventSeverity.INFO, "Network updated with Public Certificate successfully.",
            context.getOwnerIdentity().toString()));
  }

  @ExceptionHandler
  private void logErrors(Throwable t, Context context) throws Throwable {

    log.error("Error on enabling public certificate on Network: {} , Exception details:{}", context.getNetworkId(), t);


  }


  @Data
  @RequiredArgsConstructor
  public static class Context {

    private UUID ownerIdentity;

    private Boolean publicCertEnabled;

    private UUID networkId;

    private UUID networkControllerId;

    public Context(@NonNull UUID ownerIdentity, Boolean publicCertEnabled, UUID networkId) {
      this.ownerIdentity = ownerIdentity;
      this.publicCertEnabled = publicCertEnabled;
      this.networkId = networkId;
    }

  }

  @Metadata
  public JsonNode getProcessMetadata(Context context) {
    final ObjectMapper objectMapper = new ObjectMapper();
    final String networkId = Optional.ofNullable(context.getNetworkId()).map(UUID::toString).orElse("");
    return objectMapper.valueToTree(Map.of(
        "networkId", networkId,
        "resourceType", Network.class.getName(),
        "resourceId", networkId,
        "ownerIdentityId", context.getOwnerIdentity()));
  }

}
