package jsonschema

import (
	"errors"
	"fmt"
	"regexp"
	"strconv"
)

// This error indicates an failure to parse a string as a particular JSON schema type.
type parseStringError struct {
	// Expected JSON schema type for the value
	ExpectedType Type

	// The string value that failed to parse
	Value string
}

func (e parseStringError) Error() string {
	return fmt.Sprintf("%q is not a %s", e.Value, e.ExpectedType)
}

// function to check whether a float value represents an integer
func isIntegerValue(v float64) bool {
	return v == float64(int64(v))
}

func toInteger(v any) (int64, error) {
	switch typedVal := v.(type) {
	// cast float to int
	case float32:
		if !isIntegerValue(float64(typedVal)) {
			return 0, fmt.Errorf("expected integer value, got: %v", v)
		}
		return int64(typedVal), nil
	case float64:
		if !isIntegerValue(typedVal) {
			return 0, fmt.Errorf("expected integer value, got: %v", v)
		}
		return int64(typedVal), nil

	// pass through common integer cases
	case int:
		return int64(typedVal), nil
	case int32:
		return int64(typedVal), nil
	case int64:
		return typedVal, nil

	default:
		return 0, fmt.Errorf("cannot convert %#v to an integer", v)
	}
}

func toString(v any, T Type) (string, error) {
	switch T {
	case BooleanType:
		boolVal, ok := v.(bool)
		if !ok {
			return "", fmt.Errorf("expected bool, got: %#v", v)
		}
		return strconv.FormatBool(boolVal), nil
	case StringType:
		strVal, ok := v.(string)
		if !ok {
			return "", fmt.Errorf("expected string, got: %#v", v)
		}
		return strVal, nil
	case NumberType:
		floatVal, ok := v.(float64)
		if !ok {
			return "", fmt.Errorf("expected float, got: %#v", v)
		}
		return strconv.FormatFloat(floatVal, 'f', -1, 64), nil
	case IntegerType:
		intVal, err := toInteger(v)
		if err != nil {
			return "", err
		}
		return strconv.FormatInt(intVal, 10), nil
	case ArrayType, ObjectType:
		return "", fmt.Errorf("cannot format object of type %s as a string. Value of object: %#v", T, v)
	default:
		return "", fmt.Errorf("unknown json schema type: %q", T)
	}
}

func toStringSlice(arr []any, T Type) ([]string, error) {
	res := []string{}
	for _, v := range arr {
		s, err := toString(v, T)
		if err != nil {
			return nil, err
		}
		res = append(res, s)
	}
	return res, nil
}

func fromString(s string, T Type) (any, error) {
	if T == StringType {
		return s, nil
	}

	// Variables to store value and error from parsing
	var v any
	var err error

	switch T {
	case BooleanType:
		v, err = strconv.ParseBool(s)
	case NumberType:
		v, err = strconv.ParseFloat(s, 32)
	case IntegerType:
		v, err = strconv.ParseInt(s, 10, 64)
	case ArrayType, ObjectType:
		return "", fmt.Errorf("cannot parse string as object of type %s. Value of string: %q", T, s)
	default:
		return "", fmt.Errorf("unknown json schema type: %q", T)
	}

	// Return more readable error incase of a syntax error
	if errors.Is(err, strconv.ErrSyntax) {
		return nil, parseStringError{
			ExpectedType: T,
			Value:        s,
		}
	}
	return v, err
}

// Error indicates a value entered by the user failed to match the pattern specified
// in the template schema.
type patternMatchError struct {
	// The name of the property that failed to match the pattern
	PropertyName string

	// The value of the property that failed to match the pattern
	PropertyValue any

	// The regex pattern that the property value failed to match
	Pattern string

	// Failure message to display to the user, if specified in the template
	// schema
	FailureMessage string
}

func (e patternMatchError) Error() string {
	// If custom user error message is defined, return error with the custom message
	msg := e.FailureMessage
	if msg == "" {
		msg = fmt.Sprintf("Expected to match regex pattern: %s", e.Pattern)
	}
	return fmt.Sprintf("invalid value for %s: %q. %s", e.PropertyName, e.PropertyValue, msg)
}

func validatePatternMatch(name string, value any, propertySchema *Schema) error {
	if propertySchema.Pattern == "" {
		// Return early if no pattern is specified
		return nil
	}

	// Expect type of value to be a string
	stringValue, ok := value.(string)
	if !ok {
		return fmt.Errorf("invalid value for %s: %v. Expected a value of type string", name, value)
	}

	match, err := regexp.MatchString(propertySchema.Pattern, stringValue)
	if err != nil {
		return err
	}
	if match {
		// successful match
		return nil
	}

	return patternMatchError{
		PropertyName:   name,
		PropertyValue:  value,
		Pattern:        propertySchema.Pattern,
		FailureMessage: propertySchema.PatternMatchFailureMessage,
	}
}
