package resolver

import (
	"fmt"
	"regexp"

	"code.cloudfoundry.org/fissile/model"
	"code.cloudfoundry.org/fissile/validation"
)

// validateRoleRun tests whether required fields in the RoleRun are
// set. Note, some of the fields have type-dependent checks. Some
// issues are fixed silently.
// This works on the data generated by CalculateRoleRun
func validateRoleRun(instanceGroup *model.InstanceGroup, roleManifest *model.RoleManifest) validation.ErrorList {
	allErrs := validation.ErrorList{}

	allErrs = append(allErrs, normalizeFlightStage(*instanceGroup)...)
	allErrs = append(allErrs, validateHealthCheck(*instanceGroup)...)
	allErrs = append(allErrs, validateRoleMemory(*instanceGroup)...)
	allErrs = append(allErrs, validateRoleCPU(*instanceGroup)...)

	if instanceGroup.Run.ServiceAccount != "" {
		accountName := instanceGroup.Run.ServiceAccount
		if _, ok := roleManifest.Configuration.Authorization.Accounts[accountName]; !ok {
			allErrs = append(allErrs, validation.NotFound(
				fmt.Sprintf("instance_groups[%s].run.service-account", instanceGroup.Name), accountName))
		}
	} else {
		// Make the default ("default" (sic!)) explicit.
		instanceGroup.Run.ServiceAccount = "default"
	}

	for _, volume := range instanceGroup.Run.Volumes {
		switch volume.Type {
		case model.VolumeTypePersistent:
		case model.VolumeTypeShared:
		case model.VolumeTypeHost:
		case model.VolumeTypeNone:
		case model.VolumeTypeEmptyDir:
		default:
			allErrs = append(allErrs, validation.Invalid(
				fmt.Sprintf("instance_groups[%s].run.volumes[%s]", instanceGroup.Name, volume.Tag),
				volume.Type,
				fmt.Sprintf("Invalid volume type '%s'", volume.Type)))
		}
	}

	return allErrs
}

func validateJobReferences(instanceGroup *model.InstanceGroup) validation.ErrorList {
	allErrs := validation.ErrorList{}
	for _, job := range instanceGroup.JobReferences {
		for idx := range job.ContainerProperties.BoshContainerization.Ports {
			allErrs = append(allErrs, validateExposedPorts(instanceGroup.Name, job.Name, &job.ContainerProperties.BoshContainerization.Ports[idx])...)
		}
	}

	return allErrs
}

// normalizeFlightStage reports instance groups with a bad flightstage, and
// fixes all instance groups without a flight stage to use the default
// ('flight').
func normalizeFlightStage(instanceGroup model.InstanceGroup) validation.ErrorList {
	allErrs := validation.ErrorList{}

	// Normalize flight stage
	switch instanceGroup.Run.FlightStage {
	case "":
		instanceGroup.Run.FlightStage = model.FlightStageFlight
	case model.FlightStagePreFlight:
	case model.FlightStageFlight:
	case model.FlightStagePostFlight:
	case model.FlightStageManual:
	default:
		allErrs = append(allErrs, validation.Invalid(
			fmt.Sprintf("instance_groups[%s].run.flight-stage", instanceGroup.Name),
			instanceGroup.Run.FlightStage,
			"Expected one of flight, manual, post-flight, or pre-flight"))
	}

	return allErrs
}

// validateHealthCheck reports a instance group with conflicting health checks
// in its probes
func validateHealthCheck(instanceGroup model.InstanceGroup) validation.ErrorList {
	allErrs := validation.ErrorList{}

	if instanceGroup.Run.HealthCheck == nil {
		// No health checks, nothing to validate
		return allErrs
	}

	// Ensure that we don't have conflicting health checks
	if instanceGroup.Run.HealthCheck.Readiness != nil {
		allErrs = append(allErrs,
			validateHealthProbe(instanceGroup, "readiness",
				instanceGroup.Run.HealthCheck.Readiness)...)
	}
	if instanceGroup.Run.HealthCheck.Liveness != nil {
		allErrs = append(allErrs,
			validateHealthProbe(instanceGroup, "liveness",
				instanceGroup.Run.HealthCheck.Liveness)...)
	}

	return allErrs
}

// validateRoleMemory validates memory requests and limits, and
// converts the old key (`memory`, run.MemRequest), to the new
// form. Afterward only run.Memory is valid.
func validateRoleMemory(instanceGroup model.InstanceGroup) validation.ErrorList {
	allErrs := validation.ErrorList{}

	if instanceGroup.Run.Memory == nil {
		if instanceGroup.Run.MemRequest != nil {
			allErrs = append(allErrs, validation.ValidateNonnegativeField(*instanceGroup.Run.MemRequest,
				fmt.Sprintf("instance_groups[%s].run.memory", instanceGroup.Name))...)
		}
		instanceGroup.Run.Memory = &model.RoleRunMemory{Request: instanceGroup.Run.MemRequest}
		return allErrs
	}

	// assert: role.Run.Memory != nil

	if instanceGroup.Run.Memory.Request == nil {
		if instanceGroup.Run.MemRequest != nil {
			allErrs = append(allErrs, validation.ValidateNonnegativeField(*instanceGroup.Run.MemRequest,
				fmt.Sprintf("instance_groups[%s].run.memory", instanceGroup.Name))...)
		}
		instanceGroup.Run.Memory.Request = instanceGroup.Run.MemRequest
	} else {
		allErrs = append(allErrs, validation.ValidateNonnegativeField(*instanceGroup.Run.Memory.Request,
			fmt.Sprintf("instance_groups[%s].run.mem.request", instanceGroup.Name))...)
	}

	if instanceGroup.Run.Memory.Limit != nil {
		allErrs = append(allErrs, validation.ValidateNonnegativeField(*instanceGroup.Run.Memory.Limit,
			fmt.Sprintf("instance_groups[%s].run.mem.limit", instanceGroup.Name))...)
	}

	return allErrs
}

// validateRoleCPU validates cpu requests and limits, and converts the
// old key (`virtual-cpus`, run.VirtualCPUs), to the new
// form. Afterward only run.CPU is valid.
func validateRoleCPU(instanceGroup model.InstanceGroup) validation.ErrorList {
	allErrs := validation.ErrorList{}

	if instanceGroup.Run.CPU == nil {
		if instanceGroup.Run.VirtualCPUs != nil {
			allErrs = append(allErrs, validation.ValidateNonnegativeFieldFloat(*instanceGroup.Run.VirtualCPUs,
				fmt.Sprintf("instance_groups[%s].run.virtual-cpus", instanceGroup.Name))...)
		}
		instanceGroup.Run.CPU = &model.RoleRunCPU{Request: instanceGroup.Run.VirtualCPUs}
		return allErrs
	}

	// assert: role.Run.CPU != nil

	if instanceGroup.Run.CPU.Request == nil {
		if instanceGroup.Run.VirtualCPUs != nil {
			allErrs = append(allErrs, validation.ValidateNonnegativeFieldFloat(*instanceGroup.Run.VirtualCPUs,
				fmt.Sprintf("instance_groups[%s].run.virtual-cpus", instanceGroup.Name))...)
		}
		instanceGroup.Run.CPU.Request = instanceGroup.Run.VirtualCPUs
	} else {
		allErrs = append(allErrs, validation.ValidateNonnegativeFieldFloat(*instanceGroup.Run.CPU.Request,
			fmt.Sprintf("instance_groups[%s].run.cpu.request", instanceGroup.Name))...)
	}

	if instanceGroup.Run.CPU.Limit != nil {
		allErrs = append(allErrs, validation.ValidateNonnegativeFieldFloat(*instanceGroup.Run.CPU.Limit,
			fmt.Sprintf("instance_groups[%s].run.cpu.limit", instanceGroup.Name))...)
	}

	return allErrs
}

// validateExposedPorts validates exposed port ranges. It also translates the legacy
// format of port ranges ("2000-2010") into the FirstPort and Count values.
func validateExposedPorts(name, jobName string, exposedPorts *model.JobExposedPort) validation.ErrorList {
	allErrs := validation.ErrorList{}

	fieldName := fmt.Sprintf("instance_groups[%s].jobs[%s].properties.bosh_containerization.ports[%s]", name, jobName, exposedPorts.Name)

	// Validate Name
	if exposedPorts.Name == "" {
		allErrs = append(allErrs, validation.Required(fieldName+".name", ""))
	} else if regexp.MustCompile("^[a-z0-9]+(-[a-z0-9]+)*$").FindString(exposedPorts.Name) == "" {
		allErrs = append(allErrs, validation.Invalid(fieldName+".name", exposedPorts.Name,
			"port names must be lowercase words separated by hyphens"))
	}
	if len(exposedPorts.Name) > 15 {
		allErrs = append(allErrs, validation.Invalid(fieldName+".name", exposedPorts.Name,
			"port name must be no more than 15 characters"))
	} else if len(exposedPorts.Name) > 9 && exposedPorts.CountIsConfigurable {
		// need to be able to append "-12345" and still be 15 chars or less
		allErrs = append(allErrs, validation.Invalid(fieldName+".name", exposedPorts.Name,
			"user configurable port name must be no more than 9 characters"))
	}

	// Validate Protocol
	allErrs = append(allErrs, validation.ValidateProtocol(exposedPorts.Protocol, fieldName+".protocol")...)

	// Validate Internal
	firstPort, lastPort, errs := validation.ValidatePortRange(exposedPorts.Internal, fieldName+".internal")
	allErrs = append(allErrs, errs...)
	exposedPorts.InternalPort = firstPort

	if exposedPorts.Count == 0 {
		exposedPorts.Count = lastPort + 1 - firstPort
	} else if lastPort+1-firstPort != exposedPorts.Count {
		allErrs = append(allErrs, validation.Invalid(fieldName+".count", exposedPorts.Count,
			fmt.Sprintf("count doesn't match port range %s", exposedPorts.Internal)))
	}

	// Validate External
	if exposedPorts.External == "" {
		exposedPorts.ExternalPort = exposedPorts.InternalPort
	} else {
		firstPort, lastPort, errs := validation.ValidatePortRange(exposedPorts.External, fieldName+".external")
		allErrs = append(allErrs, errs...)
		exposedPorts.ExternalPort = firstPort

		if lastPort+1-firstPort != exposedPorts.Count {
			allErrs = append(allErrs, validation.Invalid(fieldName+".external", exposedPorts.External,
				"internal and external ranges must have same number of ports"))
		}
	}

	if exposedPorts.Max == 0 {
		exposedPorts.Max = exposedPorts.Count
	}

	// Validate default port count; actual count will be validated at deploy time
	if exposedPorts.Count > exposedPorts.Max {
		allErrs = append(allErrs, validation.Invalid(fieldName+".count", exposedPorts.Count,
			fmt.Sprintf("default number of ports %d is larger than max allowed %d",
				exposedPorts.Count, exposedPorts.Max)))
	}

	// Clear out legacy fields to make sure they aren't still be used elsewhere in the code
	exposedPorts.Internal = ""
	exposedPorts.External = ""

	return allErrs
}
