// Copyright 2018 the Service Broker Project Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package broker

import (
	"fmt"
	"maps"
	"sort"
	"strings"
	"unicode"

	"errors"

	"github.com/cloudfoundry/cloud-service-broker/v2/pkg/validation"
	"github.com/cloudfoundry/cloud-service-broker/v2/pkg/varcontext/interpolation"
	"github.com/cloudfoundry/cloud-service-broker/v2/utils"
	"github.com/hashicorp/go-multierror"
	"github.com/xeipuuv/gojsonschema"
)

const (
	JSONTypeString  JSONType = "string"
	JSONTypeNumeric JSONType = "number"
	JSONTypeInteger JSONType = "integer"
	JSONTypeBoolean JSONType = "boolean"
)

type JSONType string

type BrokerVariable struct {
	// Is this variable required?
	Required bool `yaml:"required,omitempty"`
	// The name of the JSON field this variable serializes/deserializes to
	FieldName string `yaml:"field_name"`
	// The JSONSchema type of the field
	Type JSONType `yaml:"type"`
	// Whether the value can be set to 'null'
	Nullable bool `yaml:"nullable,omitempty"`
	// Human-readable info about the field.
	Details string `yaml:"details"`
	// The default value of the field.
	Default any `yaml:"default,omitempty"`
	// If there are a limited number of valid values for this field then
	// Enum will hold them in value:friendly name pairs
	Enum map[any]string `yaml:"enum,omitempty"`
	// Constraints holds JSON Schema validations defined for this variable.
	// Keys are valid JSON Schema validation keywords, and values are their
	// associated values.
	// http://json-schema.org/latest/json-schema-validation.html
	Constraints     map[string]any `yaml:"constraints,omitempty"`
	ProhibitUpdate  bool           `yaml:"prohibit_update,omitempty"`
	TFAttribute     string         `yaml:"tf_attribute,omitempty"`
	TFAttributeSkip string         `yaml:"tf_attribute_skip,omitempty"`
}

// ImportVariable Variable definition for TF import support
type ImportVariable struct {
	Name       string `yaml:"field_name"`
	Type       string `yaml:"type"`
	Details    string `yaml:"details"`
	TfResource string `yaml:"tf_resource"`
}

var _ validation.Validatable = (*ServiceDefinition)(nil)

// Validate implements validation.Validatable.
func (bv *BrokerVariable) Validate() (errs *validation.FieldError) {
	if bv.TFAttribute != "" {
		errs = errs.Also(
			validation.ErrIfNotTerraformAttributePath(bv.TFAttribute, "tf_attribute"),
		)
	}

	return errs.Also(
		validation.ErrIfBlank(bv.FieldName, "field_name"),
		validation.ErrIfNotJSONSchemaType(string(bv.Type), "type"),
		validation.ErrIfBlank(bv.Details, "details"),
	)
}

// ToSchema converts the BrokerVariable into the value part of a JSON Schema.
func (bv *BrokerVariable) ToSchema() map[string]any {
	schema := map[string]any{}

	// Setting the auto-generated title comes first so it can be overridden
	// manually by constraints in special cases.
	if bv.FieldName != "" {
		schema[validation.KeyTitle] = fieldNameToLabel(bv.FieldName)
	}

	maps.Copy(schema, bv.Constraints)

	if len(bv.Enum) > 0 {
		enumeration := []any{}
		for k := range bv.Enum {
			enumeration = append(enumeration, k)
		}

		if bv.Nullable {
			enumeration = append(enumeration, nil)
		}

		// Sort enumerations lexocographically for documentation consistency.
		sort.Slice(enumeration, func(i int, j int) bool {
			return fmt.Sprintf("%v", enumeration[i]) < fmt.Sprintf("%v", enumeration[j])
		})

		schema[validation.KeyEnum] = enumeration
	}

	if bv.Details != "" {
		schema[validation.KeyDescription] = bv.Details
	}

	if bv.Type != "" {
		if bv.Nullable {
			schema[validation.KeyType] = []string{string(bv.Type), "null"}
		} else {
			schema[validation.KeyType] = bv.Type
		}
	}

	if bv.Default != nil {
		// HIL values shouldn't get set as Defaults, instead they should be added to the description field.
		if defaultString, ok := bv.Default.(string); ok && interpolation.IsHILExpression(defaultString) {
			genDesc := fmt.Sprintf("%s If you do not specify this field, it will be generated by the template %q", bv.Details, defaultString)
			schema[validation.KeyDescription] = strings.TrimSpace(genDesc)
		} else {
			schema[validation.KeyDefault] = bv.Default
		}
	}

	if bv.ProhibitUpdate {
		schema[validation.KeyProhibitUpdate] = bv.ProhibitUpdate
	}

	if bv.TFAttribute != "" {
		schema[validation.KeyTFAttribute] = bv.TFAttribute
	}

	if bv.TFAttributeSkip != "" {
		schema[validation.KeyTFAttributeSkip] = bv.TFAttributeSkip
	}

	return schema
}

func fieldNameToLabel(fieldName string) string {
	acronyms := map[string]string{
		"id":   "ID",
		"uri":  "URI",
		"url":  "URL",
		"gb":   "GB",
		"jdbc": "JDBC",
	}

	components := strings.FieldsFunc(fieldName, func(c rune) bool {
		return unicode.IsSpace(c) || c == '-' || c == '_' || c == '.'
	})

	for i, c := range components {
		if replace, ok := acronyms[c]; ok {
			components[i] = replace
		} else {
			components[i] = strings.ToUpper(c[:1]) + c[1:]
		}
	}

	return strings.Join(components, " ")
}

// ApplyDefaults adds default values for missing broker variables.
func ApplyDefaults(parameters map[string]any, variables []BrokerVariable) {
	for _, v := range variables {
		if _, ok := parameters[v.FieldName]; !ok && v.Default != nil {
			parameters[v.FieldName] = v.Default
		}
	}

}

func ValidateVariables(parameters map[string]any, variables []BrokerVariable) error {
	schema := CreateJSONSchema(variables)
	return ValidateVariablesAgainstSchema(parameters, schema)
}

// ValidateVariablesAgainstSchema validates a list of BrokerVariables are adhering to their JSONSchema.
func ValidateVariablesAgainstSchema(parameters map[string]any, schema map[string]any) error {

	result, err := gojsonschema.Validate(gojsonschema.NewGoLoader(schema), gojsonschema.NewGoLoader(parameters))
	if err != nil {
		return err
	}

	resultErrors := result.Errors()
	if len(resultErrors) == 0 {
		return nil
	}

	allErrors := &multierror.Error{
		ErrorFormat: utils.SingleLineErrorFormatter,
	}

	for _, r := range resultErrors {
		multierror.Append(allErrors, errors.New(r.String()))
	}

	return allErrors
}

// CreateJSONSchema outputs a JSONSchema given a list of BrokerVariables
func CreateJSONSchema(schemaVariables []BrokerVariable) map[string]any {
	required := utils.NewStringSet()
	properties := make(map[string]any)

	for _, variable := range schemaVariables {
		properties[variable.FieldName] = variable.ToSchema()
		if variable.Required {
			required.Add(variable.FieldName)
		}
	}

	schema := map[string]any{
		"$schema":    "http://json-schema.org/draft-04/schema#",
		"type":       "object",
		"properties": properties,
	}

	if !required.IsEmpty() {
		schema["required"] = required.ToSlice()
	}

	return schema
}
