// Copyright 2022 Princess B33f Heavy Industries / Dave Shanley
// SPDX-License-Identifier: MIT

package v2

import (
	"context"
	"crypto/sha256"
	"strings"
	"sync"

	"github.com/pb33f/libopenapi/datamodel"
	"github.com/pb33f/libopenapi/datamodel/low"
	"github.com/pb33f/libopenapi/index"
	"github.com/pb33f/libopenapi/orderedmap"
	"github.com/pb33f/libopenapi/utils"
	"gopkg.in/yaml.v3"
)

// Paths represents a low-level Swagger / OpenAPI Paths object.
type Paths struct {
	PathItems  *orderedmap.Map[low.KeyReference[string], low.ValueReference[*PathItem]]
	Extensions *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]]
}

// GetExtensions returns all Paths extensions and satisfies the low.HasExtensions interface.
func (p *Paths) GetExtensions() *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] {
	return p.Extensions
}

// FindPath attempts to locate a PathItem instance, given a path key.
func (p *Paths) FindPath(path string) (result *low.ValueReference[*PathItem]) {
	for pair := orderedmap.First(p.PathItems); pair != nil; pair = pair.Next() {
		if pair.Key().Value == path {
			result = pair.ValuePtr()
			break
		}
	}
	return result
}

// FindPathAndKey attempts to locate a PathItem instance, given a path key.
func (p *Paths) FindPathAndKey(path string) (key *low.KeyReference[string], value *low.ValueReference[*PathItem]) {
	for pair := orderedmap.First(p.PathItems); pair != nil; pair = pair.Next() {
		if pair.Key().Value == path {
			key = pair.KeyPtr()
			value = pair.ValuePtr()
			break
		}
	}
	return key, value
}

// FindExtension will attempt to locate an extension value given a name.
func (p *Paths) FindExtension(ext string) *low.ValueReference[*yaml.Node] {
	return low.FindItemInOrderedMap(ext, p.Extensions)
}

// Build will extract extensions and paths from node.
func (p *Paths) Build(ctx context.Context, _, root *yaml.Node, idx *index.SpecIndex) error {
	root = utils.NodeAlias(root)
	utils.CheckForMergeNodes(root)
	p.Extensions = low.ExtractExtensions(root)

	// Translate YAML nodes to pathsMap using `TranslatePipeline`.
	type pathBuildResult struct {
		key   low.KeyReference[string]
		value low.ValueReference[*PathItem]
	}
	type buildInput struct {
		currentNode *yaml.Node
		pathNode    *yaml.Node
	}
	pathsMap := orderedmap.New[low.KeyReference[string], low.ValueReference[*PathItem]]()
	in := make(chan buildInput)
	out := make(chan pathBuildResult)
	done := make(chan struct{})
	var wg sync.WaitGroup
	wg.Add(2) // input and output goroutines.

	// TranslatePipeline input.
	go func() {
		defer func() {
			close(in)
			wg.Done()
		}()
		skip := false
		var currentNode *yaml.Node
		for i, pathNode := range root.Content {
			if strings.HasPrefix(strings.ToLower(pathNode.Value), "x-") {
				skip = true
				continue
			}
			if skip {
				skip = false
				continue
			}
			if i%2 == 0 {
				currentNode = pathNode
				continue
			}

			select {
			case in <- buildInput{
				currentNode: currentNode,
				pathNode:    pathNode,
			}:
			case <-done:
				return
			}
		}
	}()

	// TranslatePipeline output.
	go func() {
		for {
			result, ok := <-out
			if !ok {
				break
			}
			pathsMap.Set(result.key, result.value)
		}
		close(done)
		wg.Done()
	}()

	translateFunc := func(value buildInput) (pathBuildResult, error) {
		pNode := value.pathNode
		cNode := value.currentNode
		path := new(PathItem)
		_ = low.BuildModel(pNode, path)
		err := path.Build(ctx, cNode, pNode, idx)
		if err != nil {
			return pathBuildResult{}, err
		}
		return pathBuildResult{
			key: low.KeyReference[string]{
				Value:   cNode.Value,
				KeyNode: cNode,
			},
			value: low.ValueReference[*PathItem]{
				Value:     path,
				ValueNode: pNode,
			},
		}, nil
	}
	err := datamodel.TranslatePipeline[buildInput, pathBuildResult](in, out, translateFunc)
	wg.Wait()
	if err != nil {
		return err
	}

	p.PathItems = pathsMap
	return nil
}

// Hash will return a consistent SHA256 Hash of the PathItem object
func (p *Paths) Hash() [32]byte {
	var f []string
	for v := range orderedmap.SortAlpha(p.PathItems).ValuesFromOldest() {
		f = append(f, low.GenerateHashString(v.Value))
	}
	f = append(f, low.HashExtensions(p.Extensions)...)
	return sha256.Sum256([]byte(strings.Join(f, "|")))
}
