package spec

import (
	"context"
	"fmt"
	"sort"
	"strings"
)

// Paths is specified by OpenAPI/Swagger standard version 3.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#paths-object
type Paths map[string]*PathItem

// Validate returns an error if Paths does not comply with the OpenAPI spec.
func (paths Paths) Validate(ctx context.Context) error {
	normalizedPaths := make(map[string]string, len(paths))

	keys := make([]string, 0, len(paths))
	for key := range paths {
		keys = append(keys, key)
	}
	sort.Strings(keys)
	for _, path := range keys {
		pathItem := paths[path]
		if path == "" || path[0] != '/' {
			return fmt.Errorf("path %q does not start with a forward slash (/)", path)
		}

		if pathItem == nil {
			paths[path] = &PathItem{}
			pathItem = paths[path]
		}

		normalizedPath, _, varsInPath := normalizeTemplatedPath(path)
		if oldPath, ok := normalizedPaths[normalizedPath]; ok {
			return fmt.Errorf("conflicting paths %q and %q", path, oldPath)
		}
		normalizedPaths[path] = path

		var commonParams []string
		for _, parameterRef := range pathItem.Parameters {
			if parameterRef != nil {
				if parameter := parameterRef; parameter != nil && parameter.In == ParameterInPath {
					commonParams = append(commonParams, parameter.Name)
				}
			}
		}
		operations := pathItem.Operations()
		methods := make([]string, 0, len(operations))
		for method := range operations {
			methods = append(methods, method)
		}
		sort.Strings(methods)
		for _, method := range methods {
			operation := operations[method]
			var setParams []string
			for _, parameterRef := range operation.Parameters {
				if parameterRef != nil {
					if parameter := parameterRef; parameter != nil && parameter.In == ParameterInPath {
						setParams = append(setParams, parameter.Name)
					}
				}
			}
			if expected := len(setParams) + len(commonParams); expected != len(varsInPath) {
				expected -= len(varsInPath)
				if expected < 0 {
					expected *= -1
				}
				missing := make(map[string]struct{}, expected)
				definedParams := append(setParams, commonParams...)
				for _, name := range definedParams {
					if _, ok := varsInPath[name]; !ok {
						missing[name] = struct{}{}
					}
				}
				for name := range varsInPath {
					got := false
					for _, othername := range definedParams {
						if othername == name {
							got = true
							break
						}
					}
					if !got {
						missing[name] = struct{}{}
					}
				}
				if len(missing) != 0 {
					missings := make([]string, 0, len(missing))
					for name := range missing {
						missings = append(missings, name)
					}
					return fmt.Errorf("operation %s %s must define exactly all path parameters (missing: %v)", method, path, missings)
				}
			}
		}

		if err := pathItem.Validate(ctx); err != nil {
			return fmt.Errorf("invalid path %s: %v", path, err)
		}
	}

	if err := paths.validateUniqueOperationIDs(); err != nil {
		return err
	}

	return nil
}

// Find returns a path that matches the key.
//
// The method ignores differences in template variable names (except possible "*" suffix).
//
// For example:
//
//	paths := Paths {
//	  "/person/{personName}": &PathItem{},
//	}
//	pathItem := path.Find("/person/{name}")
//
// would return the correct path item.
func (paths Paths) Find(key string) *PathItem {
	// Try directly access the map
	pathItem := paths[key]
	if pathItem != nil {
		return pathItem
	}

	normalizedPath, expected, _ := normalizeTemplatedPath(key)
	for path, pathItem := range paths {
		pathNormalized, got, _ := normalizeTemplatedPath(path)
		if got == expected && pathNormalized == normalizedPath {
			return pathItem
		}
	}
	return nil
}

func (paths Paths) validateUniqueOperationIDs() error {
	operationIDs := make(map[string]string)
	for urlPath, pathItem := range paths {
		if pathItem == nil {
			continue
		}
		for httpMethod, operation := range pathItem.Operations() {
			if operation == nil || operation.OperationID == "" {
				continue
			}
			endpoint := httpMethod + " " + urlPath
			if endpointDup, ok := operationIDs[operation.OperationID]; ok {
				if endpoint > endpointDup { // For make error message a bit more deterministic. May be useful for tests.
					endpoint, endpointDup = endpointDup, endpoint
				}
				return fmt.Errorf("operations %q and %q have the same operation id %q",
					endpoint, endpointDup, operation.OperationID)
			}
			operationIDs[operation.OperationID] = endpoint
		}
	}
	return nil
}

func normalizeTemplatedPath(path string) (string, uint, map[string]struct{}) {
	if strings.IndexByte(path, '{') < 0 {
		return path, 0, nil
	}

	var buffTpl strings.Builder
	buffTpl.Grow(len(path))

	var (
		cc         rune
		count      uint
		isVariable bool
		vars       = make(map[string]struct{})
		buffVar    strings.Builder
	)
	for i, c := range path {
		if isVariable {
			if c == '}' {
				// End path variable
				isVariable = false

				vars[buffVar.String()] = struct{}{}
				buffVar = strings.Builder{}

				// First append possible '*' before this character
				// The character '}' will be appended
				if i > 0 && cc == '*' {
					buffTpl.WriteRune(cc)
				}
			} else {
				buffVar.WriteRune(c)
				continue
			}

		} else if c == '{' {
			// Begin path variable
			isVariable = true

			// The character '{' will be appended
			count++
		}

		// Append the character
		buffTpl.WriteRune(c)
		cc = c
	}
	return buffTpl.String(), count, vars
}
