package io.netfoundry.core.process;

import static io.netfoundry.core.domain.host.HostType.EDGE_ROUTER;
import static java.lang.Boolean.TRUE;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.util.Assert;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.netfoundry.core.cloud.CloudProviderType;
import io.netfoundry.core.domain.Status;
import io.netfoundry.core.domain.edgerouter.EdgeRouter;
import io.netfoundry.core.domain.edgerouter.EdgeRouterService;
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.external.dns.NetFoundryDomainNameService;
import io.netfoundry.core.external.ziti.client.ZitiApiClientFactory;
import io.netfoundry.processing.ProcessResult;
import io.netfoundry.processing.stereotypes.CancellationHandler;
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 io.netfoundry.ziti.domain.edgerouter.ZitiEdgeRouter;
import lombok.Builder;
import lombok.NonNull;
import lombok.extern.jackson.Jacksonized;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Process(name = "Create Hosted Edge Router", description = "{{'Create Hosted Edge Router in ' + #context.region}}")
public class CreateHostedEdgeRouterProcessbck {

  private final EdgeRouterService edgeRouterService;
  private final NetworkService networkService;
  private final HostService hostService;
  private final NetFoundryDomainNameService domainNameService;
  private final EdgeRouterSoftwareInstallProcess edgeRouterSoftwareInstallProcess;
  private final String defaultEdgeRouterSize;
  private final RetryTemplate retryTemplate;
  private final ZitiApiClientFactory zitiApiClientFactory;

  private final CreateHostProcess createHostProcess;
  private final DeleteHostProcess deleteHostProcess;

  @Autowired
  public CreateHostedEdgeRouterProcessbck(
      EdgeRouterService edgeRouterService,
      NetworkService networkService,
      HostService hostService,
      NetFoundryDomainNameService domainNameService,
      EdgeRouterSoftwareInstallProcess edgeRouterSoftwareInstallProcess,
      ZitiApiClientFactory zitiApiClientFactory,
      @Value("${core.defaultEdgeRouterSize:medium}") String defaultEdgeRouterSize,
      CreateHostProcess createHostProcess,
      DeleteHostProcess deleteHostProcess) {
    this.edgeRouterService = edgeRouterService;
    this.networkService = networkService;
    this.hostService = hostService;
    this.domainNameService = domainNameService;
    this.edgeRouterSoftwareInstallProcess = edgeRouterSoftwareInstallProcess;
    this.defaultEdgeRouterSize = defaultEdgeRouterSize;
    this.createHostProcess = createHostProcess;
    this.zitiApiClientFactory = zitiApiClientFactory;
    this.deleteHostProcess = deleteHostProcess;

    this.retryTemplate = RetryTemplate.builder()
        .exponentialBackoff(250, 2, 2500)
        .maxAttempts(5)
        .build();
  }

  @Step("Start Create Hosted Edge Router")
  @Next("createHost")
  public ProcessResult<EdgeRouter> createEdgeRouter(Context context) {
    log.debug("Starting Create Hosted EdgeRouter process {}", context.networkId());

    final Network network = networkService.getById(context.networkId());
    Assert.isTrue(Status.PROVISIONED == network.getStatus(),
        String.format("Network %s is in status %s, can't add things unless network is PROVISIONED", network.getId(), network.getStatus()));

    return ProcessResult.success();
  }

  @Step("Create Host")
  @Next("registerDomain")
  private ProcessResult<UUID> createHost(Context context) {

    Network network = networkService.getById(context.networkId());

    final String size = getSizeOrDefault(context);

    return ProcessResult
        .<UUID>subprocess(() -> {
          final ProcessResult<Host> host = createHostProcess.createHost(CreateHostProcess.Context.builder()
              .ownerIdentity(context.ownerIdentity())
              .networkId(network.getId())
              .hostType(EDGE_ROUTER)
              .size(size)
              .provider(context.provider().resolve())
              .region(context.region())
              .productVersion(network.getProductVersion())
              .build());

          edgeRouterService.updateEdgeRouterHostId(context.edgeRouterId(), host.getValue().getId());
          return host;
        })
        .converter(result -> {
          if (!result.isSuccessful()) {
            throw new IllegalStateException("Unable to create host");
          }
          final UUID value = Optional.ofNullable(result.getValue())
              .map(Host.class::cast)
              .map(Host::getId)
              .orElse(null);
          return ProcessResult.value(value).success();
        })
        .build();
  }

  @Step("Register Domain Name")
  @Next("readEnrollment")
  private boolean registerDomain(Context context) {

    Network network = networkService.getById(context.networkId());

    final EdgeRouter edgeRouter = edgeRouterService.getById(context.edgeRouterId());
    Host host = hostService.getById(edgeRouter.getHostId());
    String ncDomainString = this.domainNameService.upsertPrimaryDomain(network, edgeRouter.getId(), Set.of(host.getIpAddress()));
    edgeRouterService.updateEdgeRouterDomainName(edgeRouter, ncDomainString);
    // if the request specifies a wssListener we use it, otherwise if it's an NF hosted ER and browzer is enabled default to true
    Optional.ofNullable(edgeRouter.getWssListener())
        .or(() -> Optional.of(TRUE == network.getPublicCertEnabled() && host.getProvider() != CloudProviderType.CUSTOMER))
        .filter(Boolean::booleanValue)
        .ifPresent(ignored -> this.domainNameService.upsertSecondaryDomain(network, edgeRouter.getId(), Set.of(host.getIpAddress())));
    return true;
  }

  @Step("Read Enrollment JWT")
  @Next("installEdgeRouterSoftware")
  private ProcessResult<Void> readEnrollment(Context context) {
    // make sure the enrollmentJwt is synced in MOP.  This call below will query and set from ziti if null in MOP.
    final Optional<String> enrollmentJwt = edgeRouterService.getEnrollmentJwt(context.networkId(), context.edgeRouterId());

    // reading edge router after getting enrollmentJwt ensures we get the update token included (if it exists)
    final EdgeRouter edgeRouter = edgeRouterService.getById(context.edgeRouterId());

    if (enrollmentJwt.isEmpty()) {
      log.debug("No enrollment JWT for edge router {} with ziti id {}", context.edgeRouterId(), edgeRouter.getZitiId());
      return ProcessResult.cancel("No enrollment JWT in ziti for %s, this Edge Router is most likely registered already".formatted(edgeRouter.getZitiId()));
    }

    return ProcessResult.success();
  }

  @Step("Install Edge Router Software")
  @Next("checkStatus")
  public ProcessResult<Void> installEdgeRouterSoftware(Context context) {
    final EdgeRouterSoftwareInstallProcess.Context createHostContext =
        EdgeRouterSoftwareInstallProcess.Context.builder()
            .networkId(context.networkId())
            .ownerIdentity(context.ownerIdentity())
            .edgeRouterId(context.edgeRouterId())
            .provider(context.provider())
            .region(context.region())
            .hostSize(context.hostSize())
            .build();
    return ProcessResult.<Void>subprocess(() -> edgeRouterSoftwareInstallProcess.installEdgeRouterSoftware(createHostContext)).build();
  }

  @Step("Check Status")
  private boolean checkStatus(Context context) {

    // Try to update status from ziti
    final EdgeRouter edgeRouter = edgeRouterService.getById(context.edgeRouterId());
    try {
      retryTemplate.execute(c -> {
        final Optional<ZitiEdgeRouter> zer = zitiApiClientFactory.forNetwork(context.networkId()).edge().edgeRouters().getById(edgeRouter.getZitiId());
        if (zer.isPresent() && zer.get().isVerified()) {
          edgeRouterService.updateEdgeRouter(edgeRouter, zer.get());
          return null;
        }
        throw new IllegalStateException(String.format("Network %s EdgeRouter %s unable to check ER status or status is not verified",
            context.networkId(), context.edgeRouterId()));
      });
    } catch (Exception e) {
      log.debug("Network {} EdgeRouter {} unable to update EdgeRouter status {}, ignoring and continuing",
          context.networkId(), context.edgeRouterId(), e.getMessage());
    }
    // Return success no matter if edge router is updated or not since it will be updated in the worker later
    return true;
  }

  @ExceptionHandler
  private void rollbackEdgeRouterHost(Exception e, Context context) throws Exception {
    log.error("Error creating Hosted EdgeRouter for networkId: {}", context.networkId(), e);

    final EdgeRouter edgeRouter = edgeRouterService.getById(context.edgeRouterId());
    edgeRouterService.updateEdgeRouterStatus(edgeRouter.getId(), Status.ERROR);

    deleteHost(context, edgeRouter);

    // Bubble the exception so the edge router is also cleaned up
    throw e;
  }

  @CancellationHandler
  public void onCancel(Context context) {
    log.debug("Hosted EdgeRouter {} cancelled", context.edgeRouterId());

    final EdgeRouter edgeRouter = edgeRouterService.getById(context.edgeRouterId());
    edgeRouterService.updateEdgeRouterStatus(edgeRouter.getId(), Status.ERROR);

    deleteHost(context, edgeRouter);
  }

  private void deleteHost(Context context, EdgeRouter edgeRouter) {
    if (edgeRouter.getHostId() == null) {
      log.debug("Network {} failed but the edge router has no hostId - nothing to delete", edgeRouter.getNetworkId());
      return;
    }

    final Network network = networkService.getById(context.networkId());

    log.debug("Failed to create edge router {}", edgeRouter.getId());

    try {
      log.debug("Deleting domain name records for edge router {}", edgeRouter.getId());
      this.domainNameService.deletePrimaryDomain(network, edgeRouter.getId());
      if (TRUE == edgeRouter.getWssListener()) {
        this.domainNameService.deleteSecondaryDomain(network, edgeRouter.getId());
      }
    } catch (Exception e) {
      log.debug("Failed to delete dns records for failed edge router {}, ignoring error and proceeding", edgeRouter.getId(), e);
    }

    log.debug("Deleting host {} for edge router {}", edgeRouter.getHostId(), edgeRouter.getId());

    // Call to process without subprocess() makes it async
    deleteHostProcess.deleteHost(new DeleteHostProcess.Context(edgeRouter.getNetworkId(), edgeRouter.getHostId(), context.ownerIdentity(), EDGE_ROUTER, false));
  }

  private String getSizeOrDefault(Context context) {
    if (StringUtils.isBlank(context.hostSize())) {
      log.info("No size specified in context for EdgeRouter {}, using default size {}", context.edgeRouterId(), defaultEdgeRouterSize);
      return defaultEdgeRouterSize;
    } else {
      log.info("Size {} specified in context for EdgeRouter {}", context.hostSize(), context.edgeRouterId());
      return context.hostSize();
    }
  }

  @Jacksonized
  @Builder
  public record Context(@NonNull UUID networkId, @NonNull UUID ownerIdentity, @NonNull UUID edgeRouterId,
                        @NonNull CloudProviderType provider, @NonNull String region, String hostSize) {
  }

  @Metadata
  public JsonNode getProcessMetadata(Context context) {
    final ObjectMapper objectMapper = new ObjectMapper();
    return objectMapper.valueToTree(Map.of(
        "networkId", context.networkId(),
        "resourceType", EdgeRouter.class.getName(),
        "resourceId", context.edgeRouterId(),
        "ownerIdentityId", context.ownerIdentity()
    ));
  }

}
