package com.mesosphere.sdk.offer.evaluate;

import com.mesosphere.sdk.offer.Constants;
import com.mesosphere.sdk.offer.LoggingUtils;
import com.mesosphere.sdk.offer.RangeUtils;
import com.mesosphere.sdk.offer.ResourceUtils;
import com.mesosphere.sdk.specification.NamedVIPSpec;
import com.mesosphere.sdk.specification.PortSpec;
import com.mesosphere.sdk.specification.ResourceSet;
import com.mesosphere.sdk.specification.ResourceSpec;
import com.mesosphere.sdk.specification.VolumeSpec;

import com.google.protobuf.TextFormat;
import org.apache.mesos.Protos;
import org.slf4j.Logger;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

/**
 * Handles cross-referencing a preexisting {@link Protos.TaskInfo}'s current {@link Protos.Resource}s against a set
 * of expected {@link ResourceSpec}s for that task.
 */
@SuppressWarnings({
    "checkstyle:HiddenField",
})
class TaskResourceMapper {

  private final Logger logger;

  private final Optional<String> resourceNamespace;

  private final Collection<String> taskSpecNames;

  private final List<Protos.Resource> orphanedResources = new ArrayList<>();

  private final Collection<ResourceSpec> resourceSpecs;

  private final TaskPortLookup taskPortFinder;

  private final Collection<Protos.Resource> resources;

  private final List<OfferEvaluationStage> evaluationStages;

  private final Optional<String> frameworkId;

  TaskResourceMapper(
      Collection<String> taskSpecNames,
      ResourceSet resourceSet,
      Protos.TaskInfo taskInfo,
      Optional<String> resourceNamespace,
      Optional<String> frameworkId)
  {
    logger = LoggingUtils.getLogger(getClass(), resourceNamespace);
    this.resourceNamespace = resourceNamespace;
    this.frameworkId = frameworkId;
    // Multiple tasks may share a resource set. When a resource set is updated, we want to ensure that all tasks
    // attached to the resource set receive the update.
    this.taskSpecNames = taskSpecNames;
    this.resourceSpecs = new ArrayList<>();
    this.resourceSpecs.addAll(resourceSet.getResources());
    this.resourceSpecs.addAll(resourceSet.getVolumes());
    this.taskPortFinder = new TaskPortLookup(taskInfo);
    this.resources = taskInfo.getResourcesList();
    // ONLY call this AFTER initializing all members above:
    this.evaluationStages = getEvaluationStagesInternal();
  }

  public List<Protos.Resource> getOrphanedResources() {
    return orphanedResources;
  }

  public List<OfferEvaluationStage> getEvaluationStages() {
    return evaluationStages;
  }

  private List<OfferEvaluationStage> getEvaluationStagesInternal() {
    // These are taskinfo resources which weren't found in the resourcespecs. Likely need dereservations.
    List<ResourceSpec> remainingResourceSpecs = new ArrayList<>(resourceSpecs);

    // These are resourcespecs which were matched with taskinfo resources. May need updates.
    List<ResourceLabels> matchingResources = new ArrayList<>();
    for (Protos.Resource taskResource : resources) {
      Optional<ResourceLabels> matchingResource;
      switch (taskResource.getName()) {
        case Constants.DISK_RESOURCE_TYPE:
          matchingResource = ResourceMapperUtils.findMatchingDiskSpec(
              taskResource,
              remainingResourceSpecs,
              resourceNamespace);
          break;
        case Constants.PORTS_RESOURCE_TYPE:
          matchingResource = findMatchingPortSpec(taskResource, remainingResourceSpecs);
          break;
        default:
          matchingResource = ResourceMapperUtils.findMatchingResourceSpec(
              taskResource,
              remainingResourceSpecs,
              resourceNamespace,
              frameworkId);
          break;
      }
      if (matchingResource.isPresent()) {
        if (!remainingResourceSpecs.remove(matchingResource.get().getOriginal())) {
          throw new IllegalStateException(String.format("Didn't find %s in %s",
              matchingResource.get().getOriginal(), remainingResourceSpecs));
        }
        matchingResources.add(matchingResource.get());
      } else {
        orphanedResources.add(taskResource);
      }
    }

    List<OfferEvaluationStage> stages = new ArrayList<>();

    if (!orphanedResources.isEmpty()) {
      logger.info("Unreserving orphaned task resources no longer in TaskSpec: {}",
          orphanedResources.stream().map(TextFormat::shortDebugString)
              .collect(Collectors.toList()));
    }

    if (!matchingResources.isEmpty()) {
      logger.info("Matching task/TaskSpec resources: {}", matchingResources);
      for (ResourceLabels resourceLabels : matchingResources) {
        stages.add(newUpdateEvaluationStage(taskSpecNames, resourceLabels));
      }
    }

    if (!remainingResourceSpecs.isEmpty()) {
      logger.info("Missing TaskSpec resources not found in task: {}", remainingResourceSpecs);
      for (ResourceSpec missingResource : remainingResourceSpecs) {
        stages.add(newCreateEvaluationStage(taskSpecNames, missingResource));
      }
    }
    return stages;
  }

  private Optional<ResourceLabels> findMatchingPortSpec(
      Protos.Resource taskResource, Collection<ResourceSpec> resourceSpecs)
  {
    Protos.Value.Ranges ranges = taskResource.getRanges();
    boolean hasMultiplePorts = ranges.getRangeCount() != 1
        || ranges.getRange(0).getEnd() - ranges.getRange(0).getBegin() != 0;

    if (hasMultiplePorts) {
      return Optional.empty();
    }

    Optional<String> resourceId = ResourceUtils.getResourceId(taskResource);
    if (!resourceId.isPresent()) {
      logger.error("Failed to find resource ID for resource: {}", taskResource);
      return Optional.empty();
    }

    for (ResourceSpec resourceSpec : resourceSpecs) {
      if (!(resourceSpec instanceof PortSpec)) {
        continue;
      }
      PortSpec portSpec = (PortSpec) resourceSpec;
      if (portSpec.getPort() == 0) {
        // For dynamic ports, we need to detect the port value that we had selected.
        Optional<Long> priorPort = taskPortFinder.getPriorPort(portSpec);
        if (!priorPort.isPresent()) {
          //this is a new portSpec and will never match a previously reserved taskResource
          continue;
        } else if (RangeUtils.isInAny(ranges.getRangeList(), priorPort.get())) {
          return Optional.of(new ResourceLabels(
              resourceSpec,
              resourceId.get(),
              ResourceMapperUtils.getNamespaceLabel(
                  ResourceUtils.getNamespace(taskResource),
                  resourceNamespace),
              ResourceMapperUtils.getFrameworkIdLabel(
                  ResourceUtils.getFrameworkId(taskResource),
                  frameworkId)));
        }
      } else if (RangeUtils.isInAny(ranges.getRangeList(), portSpec.getPort())) {
        // For fixed ports, we can just check for a resource whose ranges include that port.
        return Optional.of(new ResourceLabels(
            resourceSpec,
            resourceId.get(),
            ResourceMapperUtils.getNamespaceLabel(
                ResourceUtils.getNamespace(taskResource),
                resourceNamespace),
            ResourceMapperUtils.getFrameworkIdLabel(
                ResourceUtils.getFrameworkId(taskResource),
                frameworkId)));
      }
    }
    return Optional.empty();
  }

  private OfferEvaluationStage newUpdateEvaluationStage(
      Collection<String> taskSpecNames, ResourceLabels resourceLabels)
  {
    return toEvaluationStage(
        taskSpecNames,
        resourceLabels.getUpdated(),
        Optional.of(resourceLabels.getResourceId()),
        resourceLabels.getResourceNamespace(),
        resourceLabels.getPersistenceId(),
        resourceLabels.getProviderId(),
        resourceLabels.getDiskSource(),
        resourceLabels.getFrameworkId());
  }

  private OfferEvaluationStage newCreateEvaluationStage(
      Collection<String> taskSpecNames, ResourceSpec resourceSpec)
  {
    return toEvaluationStage(
        taskSpecNames,
        resourceSpec,
        Optional.empty(),
        resourceNamespace,
        Optional.empty(),
        Optional.empty(),
        Optional.empty(),
        frameworkId);
  }

  private static OfferEvaluationStage toEvaluationStage(
      Collection<String> taskSpecNames,
      ResourceSpec resourceSpec,
      Optional<String> resourceId,
      Optional<String> resourceNamespace,
      Optional<String> persistenceId,
      Optional<Protos.ResourceProviderID> providerId,
      Optional<Protos.Resource.DiskInfo.Source> diskSource,
      Optional<String> frameworkId)
  {
    if (resourceSpec instanceof NamedVIPSpec) {
      return new NamedVIPEvaluationStage(
          (NamedVIPSpec) resourceSpec, taskSpecNames, resourceId, resourceNamespace, frameworkId);
    } else if (resourceSpec instanceof PortSpec) {
      return new PortEvaluationStage(
          (PortSpec) resourceSpec,
          taskSpecNames,
          resourceId,
          resourceNamespace,
          frameworkId);
    } else if (resourceSpec instanceof VolumeSpec) {
      return VolumeEvaluationStage.getExisting(
          (VolumeSpec) resourceSpec,
          taskSpecNames,
          resourceId,
          resourceNamespace,
          persistenceId,
          providerId,
          diskSource,
          frameworkId);
    } else {
      return new ResourceEvaluationStage(
          resourceSpec,
          taskSpecNames,
          resourceId,
          resourceNamespace,
          frameworkId);
    }
  }
}