package parser

import (
	"fmt"
	"reflect"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/containous/traefik/v2/pkg/types"
)

const defaultPtrValue = "false"

// FlatOpts holds options used when encoding to Flat.
type FlatOpts struct {
	Case      string // "lower" or "upper", defaults to "lower".
	Separator string
	SkipRoot  bool
}

// Flat is a configuration item representation.
type Flat struct {
	Name        string
	Description string
	Default     string
}

// EncodeToFlat encodes a node to a Flat representation.
// Even though the given node argument should have already been augmented with metadata such as kind,
// the element (and its type information) is still needed to treat remaining edge cases.
func EncodeToFlat(element interface{}, node *Node, opts FlatOpts) ([]Flat, error) {
	if element == nil || node == nil {
		return nil, nil
	}

	if node.Kind == 0 {
		return nil, fmt.Errorf("missing node type: %s", node.Name)
	}

	elem := reflect.ValueOf(element)
	if elem.Kind() == reflect.Struct {
		return nil, fmt.Errorf("structs are not supported, use pointer instead")
	}

	encoder := encoderToFlat{FlatOpts: opts}

	var entries []Flat
	if encoder.SkipRoot {
		for _, child := range node.Children {
			field := encoder.getField(elem.Elem(), child)
			entries = append(entries, encoder.createFlat(field, child.Name, child)...)
		}
	} else {
		entries = encoder.createFlat(elem, strings.ToLower(node.Name), node)
	}

	sort.Slice(entries, func(i, j int) bool { return entries[i].Name < entries[j].Name })

	return entries, nil
}

type encoderToFlat struct {
	FlatOpts
}

func (e encoderToFlat) createFlat(field reflect.Value, name string, node *Node) []Flat {
	var entries []Flat
	if node.Kind != reflect.Map && node.Description != "-" {
		if !(node.Kind == reflect.Ptr && len(node.Children) > 0) ||
			(node.Kind == reflect.Ptr && node.Tag.Get("label") == TagLabelAllowEmpty) {
			if node.Name[0] != '[' {
				entries = append(entries, Flat{
					Name:        e.getName(name),
					Description: node.Description,
					Default:     e.getNodeValue(e.getField(field, node), node),
				})
			}
		}
	}

	for _, child := range node.Children {
		if node.Kind == reflect.Map {
			fChild := e.getField(field, child)

			var v string
			if child.Kind == reflect.Struct {
				v = defaultPtrValue
			} else {
				v = e.getNodeValue(fChild, child)
			}

			if node.Description != "-" {
				entries = append(entries, Flat{
					Name:        e.getName(name, child.Name),
					Description: node.Description,
					Default:     v,
				})
			}

			if child.Kind == reflect.Struct || child.Kind == reflect.Ptr {
				for _, ch := range child.Children {
					f := e.getField(fChild, ch)
					n := e.getName(name, child.Name, ch.Name)
					entries = append(entries, e.createFlat(f, n, ch)...)
				}
			}
		} else {
			f := e.getField(field, child)
			n := e.getName(name, child.Name)
			entries = append(entries, e.createFlat(f, n, child)...)
		}
	}

	return entries
}

func (e encoderToFlat) getField(field reflect.Value, node *Node) reflect.Value {
	switch field.Kind() {
	case reflect.Struct:
		return field.FieldByName(node.FieldName)
	case reflect.Ptr:
		if field.Elem().Kind() == reflect.Struct {
			return field.Elem().FieldByName(node.FieldName)
		}
		return field.Elem()
	case reflect.Map:
		return field.MapIndex(reflect.ValueOf(node.FieldName))
	default:
		return field
	}
}

func (e encoderToFlat) getNodeValue(field reflect.Value, node *Node) string {
	if node.Kind == reflect.Ptr && len(node.Children) > 0 {
		return defaultPtrValue
	}

	if field.Kind() == reflect.Int64 {
		i, _ := strconv.ParseInt(node.Value, 10, 64)

		switch field.Type() {
		case reflect.TypeOf(types.Duration(time.Second)):
			return strconv.Itoa(int(i) / int(time.Second))
		case reflect.TypeOf(time.Second):
			return time.Duration(i).String()
		}
	}

	return node.Value
}

func (e encoderToFlat) getName(names ...string) string {
	var name string
	if names[len(names)-1][0] == '[' {
		name = strings.Join(names, "")
	} else {
		name = strings.Join(names, e.Separator)
	}

	if strings.EqualFold(e.Case, "upper") {
		return strings.ToUpper(name)
	}
	return strings.ToLower(name)
}
