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

package index

import (
	"fmt"
	"net/url"
	"path/filepath"
	"strings"

	"github.com/pb33f/libopenapi/utils"
	"github.com/vmware-labs/yaml-jsonpath/pkg/yamlpath"
	"gopkg.in/yaml.v3"
)

// FindComponent will locate a component by its reference, returns nil if nothing is found.
// This method will recurse through remote, local and file references. For each new external reference
// a new index will be created. These indexes can then be traversed recursively.
func (index *SpecIndex) FindComponent(componentId string) *Reference {
	if index.root == nil {
		return nil
	}

	uri := strings.Split(componentId, "#/")
	if len(uri) == 2 {
		if uri[0] != "" {
			if index.specAbsolutePath == uri[0] {
				return index.FindComponentInRoot(fmt.Sprintf("#/%s", uri[1]))
			} else {
				return index.lookupRolodex(uri)
			}
		} else {
			return index.FindComponentInRoot(fmt.Sprintf("#/%s", uri[1]))
		}
	} else {

		// does it contain a file extension?
		fileExt := filepath.Ext(componentId)
		if fileExt != "" {
			return index.lookupRolodex(uri)
		}

		// root search
		return index.FindComponentInRoot(componentId)
	}
}

func FindComponent(root *yaml.Node, componentId, absoluteFilePath string, index *SpecIndex) *Reference {
	// check component for url encoding.
	if strings.Contains(componentId, "%") {
		// decode the url.
		componentId, _ = url.QueryUnescape(componentId)
	}

	name, friendlySearch := utils.ConvertComponentIdIntoFriendlyPathSearch(componentId)
	if friendlySearch == "$." {
		friendlySearch = "$"
	}
	path, err := yamlpath.NewPath(friendlySearch)
	if path == nil || err != nil || root == nil {
		return nil // no component found
	}
	res, _ := path.Find(root)

	if len(res) == 1 {
		resNode := res[0]
		fullDef := fmt.Sprintf("%s%s", absoluteFilePath, componentId)
		// extract properties

		// check if we have already seen this reference and there is a parent, use it
		var parentNode *yaml.Node
		if index.allRefs[componentId] != nil {
			parentNode = index.allRefs[componentId].ParentNode
		}
		if index.allRefs[fullDef] != nil {
			parentNode = index.allRefs[fullDef].ParentNode
		}

		ref := &Reference{
			FullDefinition:        fullDef,
			Definition:            componentId,
			Name:                  name,
			Node:                  resNode,
			Path:                  friendlySearch,
			RemoteLocation:        absoluteFilePath,
			ParentNode:            parentNode,
			Index:                 index,
			RequiredRefProperties: extractDefinitionRequiredRefProperties(resNode, map[string][]string{}, fullDef, index),
		}
		return ref
	}
	return nil
}

func (index *SpecIndex) FindComponentInRoot(componentId string) *Reference {
	if index.root != nil {
		return FindComponent(index.root, componentId, index.specAbsolutePath, index)
	}
	return nil
}

func (index *SpecIndex) lookupRolodex(uri []string) *Reference {
	if index.rolodex == nil {
		return nil
	}

	if len(uri) > 0 {

		// split string to remove file reference
		file := strings.ReplaceAll(uri[0], "file:", "")

		var absoluteFileLocation, fileName string
		fileName = filepath.Base(file)
		absoluteFileLocation = file

		// if the absolute file location has no file ext, then get the rolodex root.
		ext := filepath.Ext(absoluteFileLocation)
		var parsedDocument *yaml.Node
		var err error

		idx := index
		if ext != "" {
			// extract the document from the rolodex.
			rFile, rError := index.rolodex.Open(absoluteFileLocation)

			if rError != nil {
				index.logger.Error("unable to open the rolodex file, check specification references and base path",
					"file", absoluteFileLocation, "error", rError)
				return nil
			}

			if rFile == nil {
				index.logger.Error("cannot locate file in the rolodex, check specification references and base path",
					"file", absoluteFileLocation)
				return nil
			}
			if rFile.GetIndex() != nil {
				idx = rFile.GetIndex()
			}

			parsedDocument, err = rFile.GetContentAsYAMLNode()
			if err != nil {
				index.logger.Error("unable to parse rolodex file", "file", absoluteFileLocation, "error", err)
				return nil
			}
		} else {
			parsedDocument = index.root
		}

		wholeFile := false
		query := ""
		if len(uri) < 2 {
			wholeFile = true
		} else {
			query = fmt.Sprintf("#/%s", uri[1])
		}

		// check if there is a component we want to suck in, or if the
		// entire root needs to come in.
		var foundRef *Reference
		if wholeFile {
			if parsedDocument.Kind == yaml.DocumentNode {
				parsedDocument = parsedDocument.Content[0]
			}

			var parentNode *yaml.Node
			if index.allRefs[absoluteFileLocation] != nil {
				parentNode = index.allRefs[absoluteFileLocation].ParentNode
			}

			foundRef = &Reference{
				ParentNode:            parentNode,
				FullDefinition:        absoluteFileLocation,
				Definition:            fileName,
				Name:                  fileName,
				Index:                 idx,
				Node:                  parsedDocument,
				IsRemote:              true,
				RemoteLocation:        absoluteFileLocation,
				Path:                  "$",
				RequiredRefProperties: extractDefinitionRequiredRefProperties(parsedDocument, map[string][]string{}, absoluteFileLocation, index),
			}
			return foundRef
		} else {
			foundRef = FindComponent(parsedDocument, query, absoluteFileLocation, index)
			if foundRef != nil {
				foundRef.IsRemote = true
				foundRef.RemoteLocation = absoluteFileLocation
				return foundRef
			}
		}
	}
	return nil
}
