package telemetry

import (
	"errors"
	"fmt"
	"time"

	identifier "code.justin.tv/amzn/TwitchProcessIdentifier"
)

// SampleBuilder builds samples, and by default includes standard dimensions and rollups.
type SampleBuilder struct {
	// ProcessIdentifier identifies the process running this service.
	ProcessIdentifier identifier.ProcessIdentifier

	// OperationName is the optional name of the operation that the service is handling.
	OperationName string

	// DependencyProcessIdentifier is the optional identifier for the dependency that the service is using.
	DependencyProcessIdentifier identifier.ProcessIdentifier

	// DependencyOperationName is the optional name of the operation that is being invoked on the dependency.
	DependencyOperationName string

	// Dimensions is an optional set of dimensions that will override the defaults.
	// To remove a default dimension, add the key with an empty string value.
	Dimensions DimensionSet

	// Timestamp is optional.  The zero value will be replaced by the current time.
	Timestamp time.Time

	// EnableProcessAddressDimension adds a ProcessAddress (host) dimension and includes it in the rollups.
	// Enabling this can greatly increase the number of metrics generated.
	EnableProcessAddressDimension bool
}

// buildDimensions assembles the standard dimensions, skipping missing values, and then applies override dimensions.
func (builder *SampleBuilder) buildDimensions() (DimensionSet, error) {
	dimensions := make(DimensionSet)

	// Local process identifier
	pid := &builder.ProcessIdentifier
	if len(pid.Service) > 0 {
		dimensions[DimensionService] = pid.Service
	} else {
		return nil, errors.New("missing ProcessIdentifier.Service")
	}
	if len(pid.Stage) > 0 {
		dimensions[DimensionStage] = pid.Stage
	}
	if len(pid.Substage) > 0 {
		dimensions[DimensionSubstage] = pid.Substage
	}
	if len(pid.Region) > 0 {
		dimensions[DimensionRegion] = pid.Region
	}

	// ProcessAddress contains only the machine id to hopefully keep the cardinality down.
	// It still may have a high cardinality for Lambda and services that have EC2 instances or containers constantly
	// failing and being replaced for some reason.
	if builder.EnableProcessAddressDimension && len(pid.Machine) > 0 {
		dimensions[DimensionProcessAddress] = pid.Machine
	}

	// Local operation name
	if len(builder.OperationName) > 0 {
		dimensions[DimensionOperation] = builder.OperationName
	}

	// Dependency
	if len(builder.DependencyProcessIdentifier.Service) > 0 || len(builder.DependencyOperationName) > 0 {
		serviceName := MetricValueUnknownService
		if len(builder.DependencyProcessIdentifier.Service) > 0 {
			serviceName = builder.DependencyProcessIdentifier.Service
		}
		operationName := MetricValueUnknownOperation
		if len(builder.DependencyOperationName) > 0 {
			operationName = builder.DependencyOperationName
		}
		dimensions[DimensionDependency] = serviceName + MetricValueDependencySeparator + operationName
	}

	// Apply override dimensions
	if builder.Dimensions != nil {
		for key, value := range builder.Dimensions {
			if value == "" {
				delete(dimensions, key)
			} else {
				dimensions[key] = value
			}
		}
	}

	if len(dimensions) > MaxDimensions {
		return nil, errors.New("too many dimensions")
	}

	return dimensions, nil
}

// buildRollups assembles the standard rollups that apply to the provided dimensions.
func (builder *SampleBuilder) buildRollups(dimensions DimensionSet) [][]string {
	rollups := make([][]string, 0, 5)
	fields := make([]string, 0, 10)

	// Some example dimension name lists to consider when reading the rollups below:
	// Request metrics: [Service, Stage, Substage, Region, ProcessAddress, Operation]
	// Dependency call: [Service, Stage, Substage, Region, ProcessAddress, Operation, Dependency]
	// Custom Dimension: [Service, Stage, Substage, Region, ProcessAddress, Operation, ShardID]
	// Non-request metrics (cpu/memory): [Service, Stage, Substage, Region, ProcessAddress]
	// Single fleet service: [Service, Stage, ProcessAddress, Operation]

	// Rollup on ProcessAddress first if it exists since it is the most specific one.
	// The result is combined metrics for all hosts in the region.
	//
	// Here's the example results after applying the rollup:
	// Request metrics: [Service, Stage, Substage, Region, Operation]
	// Dependency call: [Service, Stage, Substage, Region, Operation, Dependency]
	// Custom Dimension: [Service, Stage, Substage, Region, Operation, ShardID]
	// Non-request metrics: [Service, Stage, Substage, Region]
	// Single fleet service: [Service, Stage, Operation]
	if _, ok := dimensions[DimensionProcessAddress]; ok {
		fields = append(fields, DimensionProcessAddress)
		rollups = append(rollups, fields)
	}

	// Rollup unrecognized dimensions if any exist.
	// This would be useful for getting times and counts across all values of the custom dimension (all ShardIDs).
	//
	// Here's the example results after applying the rollup:
	// Request metrics: (skipped)
	// Dependency call: (skipped)
	// Custom Dimension: [Service, Stage, Substage, Region, Operation]
	// Non-request metrics: (skipped)
	// Single fleet service: (skipped)
	unrecognizedRollupFound := false
	for dimension := range dimensions {
		if !isStandardDimension(dimension) {
			fields = append(fields, dimension)
			unrecognizedRollupFound = true
		}
	}
	if unrecognizedRollupFound {
		rollups = append(rollups, fields)
	}

	// Rollup across operation if the dimension exists.
	// This is useful for getting whole service success/error rates for the fleet or a dependency regardless of operation.
	//
	// Here's the example results after applying the rollup:
	// Request metrics: [Service, Stage, Substage, Region]
	// Dependency call: [Service, Stage, Substage, Region, Dependency]
	// Custom Dimension: [Service, Stage, Substage, Region]
	// Non-request metrics: (skipped)
	// Single fleet service: [Service, Stage]
	if _, ok := dimensions[DimensionOperation]; ok {
		fields = append(fields, DimensionOperation)
		rollups = append(rollups, fields)
	}

	// Last, rollup across substage if it exists.
	// For services with one-box or other substages, it is useful for getting overall metrics for the service.
	//
	// Here's the example results after applying the rollup:
	// Request metrics: [Service, Stage, Region]
	// Dependency call: [Service, Stage, Region, Dependency]
	// Custom Dimension: [Service, Stage, Region]
	// Non-request metrics: [Service, Stage, Region]
	// Single fleet service: (skipped)
	if _, ok := dimensions[DimensionSubstage]; ok {
		fields = append(fields, DimensionSubstage)
		rollups = append(rollups, fields)
	}

	return rollups
}

// Build returns a new sample, or a validation error.
func (builder *SampleBuilder) Build(metricName string, value float64, units string) (*Sample, error) {
	dimensions, err := builder.buildDimensions()
	if err != nil {
		return nil, fmt.Errorf("could not build dimensions for %s: %s", metricName, err.Error())
	}
	rollup := builder.buildRollups(dimensions)

	sampleTime := builder.Timestamp
	if sampleTime.IsZero() {
		sampleTime = time.Now()
	}
	return &Sample{
		MetricID:         MetricID{Name: metricName, Dimensions: dimensions},
		RollupDimensions: rollup,
		Value:            value,
		Timestamp:        sampleTime,
		Unit:             units,
	}, nil
}

// BuildDurationSample returns a new sample from a duration with appropriate units, or a validation error.
func (builder *SampleBuilder) BuildDurationSample(metricName string, duration time.Duration) (*Sample, error) {
	dimensions, err := builder.buildDimensions()
	if err != nil {
		return nil, fmt.Errorf("could not build dimensions for %s: %s", metricName, err.Error())
	}
	rollup := builder.buildRollups(dimensions)

	sampleTime := builder.Timestamp
	if sampleTime.IsZero() {
		sampleTime = time.Now()
	}
	return &Sample{
		MetricID:         MetricID{Name: metricName, Dimensions: dimensions},
		RollupDimensions: rollup,
		Value:            duration.Seconds(),
		Timestamp:        sampleTime,
		Unit:             UnitSeconds,
	}, nil
}

// BuildAvailabilitySamples returns new availability samples, or a validation error.
// If dependency information is present, then it will use dependency metric naming, otherwise local service metric naming.
func (builder *SampleBuilder) BuildAvailabilitySamples(availability AvailabilityCode) ([]*Sample, error) {
	dimensions, err := builder.buildDimensions()
	if err != nil {
		return nil, fmt.Errorf("could not build dimensions for availability metrics: %s", err.Error())
	}
	rollup := builder.buildRollups(dimensions)

	sampleTime := builder.Timestamp
	if sampleTime.IsZero() {
		sampleTime = time.Now()
	}

	// If dependency information is present, use dependency metric names.
	if _, ok := dimensions[DimensionDependency]; ok {
		return []*Sample{
			&Sample{
				MetricID:         MetricID{Name: MetricDependencySuccess, Dimensions: dimensions},
				RollupDimensions: rollup,
				Value:            boolToSampleValue(availability == AvailabilityCodeSucccess),
				Timestamp:        sampleTime,
				Unit:             UnitCount,
			},
			&Sample{
				MetricID:         MetricID{Name: MetricDependencyClientError, Dimensions: dimensions},
				RollupDimensions: rollup,
				Value:            boolToSampleValue(availability == AvailabilityCodeClientError),
				Timestamp:        sampleTime,
				Unit:             UnitCount,
			},
			&Sample{
				MetricID:         MetricID{Name: MetricDependencyServerError, Dimensions: dimensions},
				RollupDimensions: rollup,
				Value:            boolToSampleValue(availability == AvailabilityCodeServerError),
				Timestamp:        sampleTime,
				Unit:             UnitCount,
			},
		}, nil
	}

	// Otherwise, use local service metric names.
	return []*Sample{
		&Sample{
			MetricID:         MetricID{Name: MetricSuccess, Dimensions: dimensions},
			RollupDimensions: rollup,
			Value:            boolToSampleValue(availability == AvailabilityCodeSucccess),
			Timestamp:        sampleTime,
			Unit:             UnitCount,
		},
		&Sample{
			MetricID:         MetricID{Name: MetricClientError, Dimensions: dimensions},
			RollupDimensions: rollup,
			Value:            boolToSampleValue(availability == AvailabilityCodeClientError),
			Timestamp:        sampleTime,
			Unit:             UnitCount,
		},
		&Sample{
			MetricID:         MetricID{Name: MetricServerError, Dimensions: dimensions},
			RollupDimensions: rollup,
			Value:            boolToSampleValue(availability == AvailabilityCodeServerError),
			Timestamp:        sampleTime,
			Unit:             UnitCount,
		},
	}, nil
}

// boolToSampleValue returns a counter value of 0 or 1 using the provided boolean.
func boolToSampleValue(value bool) float64 {
	if value {
		return 1
	}
	return 0
}
