package io.netfoundry.core.process;

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.util.Assert;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.netfoundry.core.cloud.CloudHostService;
import io.netfoundry.core.domain.Status;
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.NetworkController;
import io.netfoundry.core.domain.networkcontroller.NetworkControllerService;
import io.netfoundry.core.domain.softwaredeploymentstate.SoftwareDeploymentStateService;
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.NetFoundryDomainNameService;
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 lombok.extern.slf4j.Slf4j;

@Slf4j
@Process("Resume NetworkController")
public class ResumeNetworkControllerProcess {

  private final NetworkControllerService networkControllerService;

  private final EventService eventService;
  private final NetworkService networkService;
  private final CloudHostService cloudHostService;
  private final HostService hostService;
  private final NetFoundryDomainNameService domainNameService;
  private final NetworkHealthCheckService networkHealthCheckService;
  private final SoftwareDeploymentStateService softwareDeploymentStateService;
  private final ReinstallNetworkControllerProcess reinstallNetworkControllerProcess;

  @Autowired
  public ResumeNetworkControllerProcess(NetworkControllerService networkControllerService,
      EventService eventService,
      NetworkService networkService,
      CloudHostService cloudHostService,
      HostService hostService,
      NetFoundryDomainNameService domainNameService,
      NetworkHealthCheckService networkHealthCheckService,
      SoftwareDeploymentStateService softwareDeploymentStateService,
      ReinstallNetworkControllerProcess reinstallNetworkControllerProcess) {
    this.networkControllerService = networkControllerService;
    this.eventService = eventService;
    this.networkService = networkService;
    this.cloudHostService = cloudHostService;
    this.hostService = hostService;
    this.domainNameService = domainNameService;
    this.networkHealthCheckService = networkHealthCheckService;
    this.softwareDeploymentStateService = softwareDeploymentStateService;
    this.reinstallNetworkControllerProcess = reinstallNetworkControllerProcess;
  }

  @Step("Update Network Controller record")
  @Next("resumeHost")
  public ProcessResult<NetworkController> resumeNetworkController(UUID networkId, UUID networkControllerId, UUID ownerIdentityId) {

    log.debug("Network {} resuming Network Controller {}", networkId, networkControllerId);

    final NetworkController networkController = networkControllerService.getById(networkControllerId);
    if (networkController.getStatus() == Status.PROVISIONED) {
      log.debug("Network Controller {} is already PROVISIONED, skipping resume", networkController.getStatus());
      return ProcessResult.value(networkController).success();
    }

    Assert.isTrue(networkController.getStatus() == Status.SUSPENDED,
        "Network Controller %s is in status %s, only SUSPENDED Network Controllers can be resumed".formatted(networkControllerId, networkController.getStatus()));
    // NC can't run customer hosted, so we just need to check we have a host id
    Assert.notNull(networkController.getHostId(), "Network Controller %s doesn't have a hostId, can't resume.".formatted(networkControllerId));

    final NetworkController updated = networkControllerService.updateNetworkControllerStatus(networkControllerId, Status.UPDATING);

    return ProcessResult.value(updated).success();
  }

  @Step("Resume host")
  @Next("updateDomainName")
  public ProcessResult<NetworkController> resumeHost(UUID networkId, UUID networkControllerId) {

    log.debug("Network {} resuming Network Controller {}", networkId, networkControllerId);

    final NetworkController networkController = networkControllerService.getById(networkControllerId);
    if (networkController.getStatus() == Status.PROVISIONED) {
      log.debug("Network Controller {} is already PROVISIONED, skipping resume", networkController.getId());
      return ProcessResult.success();
    }

    final Host host = hostService.getById(networkController.getHostId());
    if (host.getProviderInstanceId() == null) {
      log.warn("Host {} in network {} does not have an instance id, nothing to resume", networkController.getHostId(), host.getNetworkId());
      return ProcessResult.fail();
    }

    final Network network = networkService.getById(networkId);

    final String ipAddress = cloudHostService.resumeHost(network, host);
    hostService.updateProviderInfo(host, null, null, null, ipAddress, null, null);

    return ProcessResult.success();
  }

  @Step("Update Domain Name")
  @Next("reconfigureZitiController")
  public ProcessResult<NetworkController> updateDomainName(UUID networkId, UUID networkControllerId) {

    log.debug("Network {} updating domain name for Network Controller {}", networkId, networkControllerId);

    final NetworkController networkController = networkControllerService.getById(networkControllerId);
    if (networkController.getStatus() == Status.PROVISIONED) {
      log.debug("Network Controller {} is already PROVISIONED, skipping resume", networkController.getId());
      return ProcessResult.success();
    }

    final Host host = hostService.getById(networkController.getHostId());

    String domainName = domainNameService.upsertPrimaryDomain(networkService.getById(networkId), networkControllerId, Set.of(host.getIpAddress()));
    networkControllerService.updateNetworkDomainName(networkController, domainName);

    return ProcessResult.success();
  }

  @Step("Reconfigure Ziti Controller")
  @Next("finish")
  public ProcessResult<NetworkController> reconfigureZitiController(UUID networkId, UUID networkControllerId) {

    log.debug("Network {} updating domain name for Network Controller {}", networkId, networkControllerId);

    reinstallNetworkControllerProcess.reconfigureZitiController(UUID networkId, UUID networkControllerId);

    return ProcessResult.success();
  }


  @Step("Finish")
  private ProcessResult<Void> finish(UUID networkId, UUID networkControllerId, UUID ownerIdentityId) {

    log.debug("Network {} resumed NetworkController {}", networkId, networkControllerId);
    final NetworkController networkController = networkControllerService.updateNetworkControllerStatus(networkControllerId, Status.PROVISIONED);

    final Network network = networkService.updateNetworkStatus(networkId, Status.PROVISIONED);
    log.debug("Network {} has status {} after resuming network controller {}", network.getId(), network.getStatus(), networkControllerId);

    eventService.publishEvent(
        new EventRecord(MetricType.MOP_EVENT, network.getNetworkGroupId(), network.getId(), ComponentType.NETWORK_CONTROLLER, network.getId().toString(),
            network.getName(), EventType.ACTIVE, EventSeverity.INFO, "Network Controller %s resumed.".formatted(networkController.getName()), ownerIdentityId.toString()));

    softwareDeploymentStateService.scheduleRefreshOfResource(networkController.getSoftwareDeploymentStateId());

    return ProcessResult.success();
  }

  @ExceptionHandler
  private void handleException(Exception exception, UUID networkId, UUID networkControllerId, UUID ownerIdentityId) throws Exception {

    log.error("Network {} error resuming NetworkController {}", networkId, networkControllerId, exception);
    final Network network = networkService.getById(networkId);
    final NetworkController networkController = networkControllerService.getById(networkControllerId);

    // capture status before updating it
    final Status status = networkController.getStatus();

    handleFailedResume(networkId, networkControllerId);

    eventService.publishEvent(new EventRecord(MetricType.MOP_EVENT, network.getNetworkGroupId(), network.getId(), ComponentType.NETWORK_CONTROLLER, network.getId().toString(),
        network.getName(), EventType.ACTIVE, EventSeverity.ERROR, "Network Controller %s resume failed.".formatted(networkController.getName()), ownerIdentityId.toString()));

    // if status is UPDATING the exception happened during process execution, otherwise it's in the first step and we rethrow for it to bubble out to the caller of the api
    if (status != Status.UPDATING) {
      throw exception;
    }
  }

  @CancellationHandler
  private void onCancel(Exception exception, UUID networkId, UUID networkControllerId, UUID ownerIdentityId) throws Exception {

    log.info("Network {} resuming NetworkController {} cancelled", networkId, networkControllerId, exception);
    final Network network = networkService.getById(networkId);
    final NetworkController networkController = networkControllerService.getById(networkControllerId);

    handleFailedResume(networkId, networkControllerId);

    eventService.publishEvent(new EventRecord(MetricType.MOP_EVENT, network.getNetworkGroupId(), network.getId(), ComponentType.NETWORK_CONTROLLER, network.getId().toString(),
        network.getName(), EventType.ACTIVE, EventSeverity.ERROR, "Network Controller %s resume was cancelled.".formatted(networkController.getName()), ownerIdentityId.toString()));
  }

  private void handleFailedResume(UUID networkId, UUID networkControllerId) {
    networkControllerService.updateNetworkControllerStatus(networkControllerId, Status.SUSPENDED);

    if (networkControllerService.atLeastOneMatches(networkId, Status.PROVISIONED)) {
      networkService.updateNetworkStatus(networkId, Status.PROVISIONED);
    } else {
      networkService.updateNetworkStatus(networkId, Status.ERROR);
    }
  }

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

}
