package api

import (
	"errors"
	"io"
	"maps"
	"net/http"
	"os"
	"slices"

	"github.com/AlexxIT/go2rtc/internal/app"
	pkgyaml "github.com/AlexxIT/go2rtc/pkg/yaml"
	"gopkg.in/yaml.v3"
)

func configHandler(w http.ResponseWriter, r *http.Request) {
	if app.ConfigPath == "" {
		http.Error(w, "", http.StatusGone)
		return
	}

	switch r.Method {
	case "GET":
		data, err := os.ReadFile(app.ConfigPath)
		if err != nil {
			http.Error(w, "", http.StatusNotFound)
			return
		}
		// https://www.ietf.org/archive/id/draft-ietf-httpapi-yaml-mediatypes-00.html
		Response(w, data, "application/yaml")

	case "POST", "PATCH":
		if IsReadOnly() {
			ReadOnlyError(w)
			return
		}
		data, err := io.ReadAll(r.Body)
		if err != nil {
			http.Error(w, err.Error(), http.StatusBadRequest)
			return
		}

		if r.Method == "PATCH" {
			// no need to validate after merge
			data, err = mergeYAML(app.ConfigPath, data)
			if err != nil {
				http.Error(w, err.Error(), http.StatusBadRequest)
				return
			}
		} else {
			// validate config
			if err = yaml.Unmarshal(data, map[string]any{}); err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}
		}

		if err = os.WriteFile(app.ConfigPath, data, 0644); err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
	}
}

func mergeYAML(file1 string, yaml2 []byte) ([]byte, error) {
	data1, err := os.ReadFile(file1)
	if err != nil {
		return nil, err
	}

	var patch map[string]any
	if err = yaml.Unmarshal(yaml2, &patch); err != nil {
		return nil, err
	}

	data1, err = mergeYAMLMap(data1, nil, patch)
	if err != nil {
		return nil, err
	}

	// validate config after merge
	if err = yaml.Unmarshal(data1, map[string]any{}); err != nil {
		return nil, err
	}

	return data1, nil
}

// mergeYAMLMap recursively applies patch values onto config bytes.
func mergeYAMLMap(data []byte, path []string, patch map[string]any) ([]byte, error) {
	for _, key := range slices.Sorted(maps.Keys(patch)) {
		value := patch[key]
		currPath := append(append([]string(nil), path...), key)

		if valueMap, ok := value.(map[string]any); ok {
			isMap, exists, err := pathIsMapping(data, currPath)
			if err != nil {
				return nil, err
			}

			if exists && isMap {
				data, err = mergeYAMLMap(data, currPath, valueMap)
			} else {
				data, err = pkgyaml.Patch(data, currPath, valueMap)
			}
			if err != nil {
				return nil, err
			}
			continue
		}

		var err error
		data, err = pkgyaml.Patch(data, currPath, value)
		if err != nil {
			return nil, err
		}
	}
	return data, nil
}

// pathIsMapping reports whether path exists and ends with a mapping node.
func pathIsMapping(data []byte, path []string) (isMap, exists bool, err error) {
	var root yaml.Node
	if err = yaml.Unmarshal(data, &root); err != nil {
		return false, false, err
	}

	if len(root.Content) == 0 {
		return false, false, nil
	}

	if len(root.Content) != 1 || root.Content[0].Kind != yaml.MappingNode {
		return false, false, errors.New("yaml: expected mapping document")
	}

	node := root.Content[0]
	for i, part := range path {
		idx := -1
		for j := 0; j < len(node.Content); j += 2 {
			if node.Content[j].Value == part {
				idx = j
				break
			}
		}
		if idx < 0 {
			return false, false, nil
		}

		valueNode := node.Content[idx+1]
		if i == len(path)-1 {
			return valueNode.Kind == yaml.MappingNode, true, nil
		}

		if valueNode.Kind != yaml.MappingNode {
			return false, false, nil
		}

		node = valueNode
	}

	return false, false, nil
}
