// Copyright 2024 Dolthub, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package minver

import (
	"errors"
	"fmt"
	"io"
	"os"
	"reflect"
	"strings"
	"testing"

	"github.com/stretchr/testify/require"

	"github.com/dolthub/dolt/go/libraries/utils/structwalk"
	"github.com/dolthub/dolt/go/libraries/utils/version"
)

type FieldInfo struct {
	Name    string
	TypeStr string
	MinVer  string
	YamlTag string
}

func FieldInfoFromLine(l string) (FieldInfo, error) {
	l = strings.TrimSpace(l)
	tokens := strings.Split(l, " ")

	if len(tokens) != 4 {
		return FieldInfo{}, fmt.Errorf("invalid line in minver_validation.txt: '%s'", l)
	}

	return FieldInfo{
		Name:    tokens[0],
		TypeStr: tokens[1],
		MinVer:  tokens[2],
		YamlTag: tokens[3],
	}, nil
}

func FieldInfoFromStructField(field reflect.StructField, depth int) FieldInfo {
	info := FieldInfo{
		Name:    strings.Repeat("-", depth) + field.Name,
		TypeStr: strings.Replace(field.Type.String(), " ", "", -1),
		MinVer:  field.Tag.Get("minver"),
		YamlTag: field.Tag.Get("yaml"),
	}

	if info.MinVer == "" {
		info.MinVer = "0.0.0"
	}

	return info
}

func (fi FieldInfo) Equals(other FieldInfo) bool {
	return fi.Name == other.Name && fi.TypeStr == other.TypeStr && fi.MinVer == other.MinVer && fi.YamlTag == other.YamlTag
}

func (fi FieldInfo) String() string {
	return fmt.Sprintf("%s %s %s %s", fi.Name, fi.TypeStr, fi.MinVer, fi.YamlTag)
}

type MinVerValidationReader struct {
	lines   []string
	current int
}

func OpenMinVerValidation(path string) (*MinVerValidationReader, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return nil, err
	}

	lines := strings.Split(string(data), "\n")

	return &MinVerValidationReader{
		lines:   lines,
		current: -1,
	}, nil
}

func (r *MinVerValidationReader) Advance() {
	for r.current < len(r.lines) {
		r.current++

		if r.current < len(r.lines) {
			l := r.lines[r.current]

			if !strings.HasPrefix(l, "#") {
				return
			}
		}
	}
}

func (r *MinVerValidationReader) Current() (FieldInfo, error) {
	if r.current < 0 {
		r.Advance()
	}

	if r.current < 0 || r.current < len(r.lines) {
		l := r.lines[r.current]
		return FieldInfoFromLine(l)
	}

	return FieldInfo{}, io.EOF
}

func ValidateMinVerFunc(field reflect.StructField, depth int) error {
	var hasMinVer bool
	var hasOmitEmpty bool

	minVerTag := field.Tag.Get("minver")
	if minVerTag != "" {
		if minVerTag != "TBD" {
			if _, err := version.Encode(minVerTag); err != nil {
				return fmt.Errorf("invalid minver tag on field %s '%s': %w", field.Name, minVerTag, err)
			}
		}
		hasMinVer = true
	}

	isNullable := field.Type.Kind() == reflect.Ptr || field.Type.Kind() == reflect.Slice || field.Type.Kind() == reflect.Map
	if hasMinVer && !isNullable {
		return fmt.Errorf("field '%s' has a version tag '%s' but is not nullable", field.Name, minVerTag)
	}

	yamlTag := field.Tag.Get("yaml")
	if yamlTag == "" {
		return fmt.Errorf("required tag 'yaml' missing on field '%s'", field.Name)
	} else {
		vals := strings.Split(yamlTag, ",")
		for _, val := range vals {
			if val == "omitempty" {
				hasOmitEmpty = true
				break
			}
		}
	}

	if hasMinVer && !hasOmitEmpty {
		return fmt.Errorf("field '%s' has a version tag '%s' but no yaml tag with omitempty", field.Name, minVerTag)
	}

	return nil
}

func ValidateAgainstFile(t *testing.T, path string, st any) {
	rd, err := OpenMinVerValidation(path)
	require.NoError(t, err)

	rd.Advance()

	err = structwalk.Walk(st, func(field reflect.StructField, depth int) error {
		fi := FieldInfoFromStructField(field, depth)
		prevFI, err := rd.Current()
		if err != nil && !errors.Is(err, io.EOF) {
			return err
		}

		if prevFI.Equals(fi) {
			rd.Advance()
			return nil
		}

		if fi.MinVer == "TBD" {
			return nil
		}

		if errors.Is(err, io.EOF) {
			return fmt.Errorf("new field '%s' added", fi.String())
		} else {
			// You are seeing this error because a new config field was added that didn't meet the requirements.
			// See the comment in "TestMinVer" which covers the requirements of new fields.
			return fmt.Errorf("expected '%s' but got '%s'", prevFI.String(), fi.String())
		}
	})
	require.NoError(t, err)
}

func GenValidationFile(st any, outFile string) error {
	lines := []string{
		"# file automatically updated by the release process.",
		"# if you are getting an error with this file it's likely you",
		"# have added a new minver tag with a value other than TBD",
	}

	err := structwalk.Walk(st, func(field reflect.StructField, depth int) error {
		fi := FieldInfoFromStructField(field, depth)
		lines = append(lines, fi.String())
		return nil
	})

	if err != nil {
		return fmt.Errorf("error generating data for '%s': %w", outFile, err)
	}

	fileContents := strings.Join(lines, "\n")

	err = os.WriteFile(outFile, []byte(fileContents), 0644)
	if err != nil {
		return fmt.Errorf("error writing '%s': %w", outFile, err)
	}

	return nil
}
