package output

import (
	"fmt"
	"io"
	"reflect"
	"sort"
	"strings"
	"unicode"

	"github.com/fatih/structs"
	"github.com/jedib0t/go-pretty/v6/table"
	"github.com/spf13/cobra"

	"github.com/hetznercloud/cli/internal/cmd/cmpl"
	"github.com/hetznercloud/cli/internal/cmd/util"
)

const flagName = "output"

type Option struct {
	Name   string
	Values []string
}

func OptionNoHeader() Option {
	return Option{Name: "noheader"}
}

func OptionJSON() Option {
	return Option{Name: "json"}
}

func OptionYAML() Option {
	return Option{Name: "yaml"}
}

func OptionFormat() Option {
	return Option{Name: "format"}
}

func OptionColumns(columns []string) Option {
	return Option{Name: "columns", Values: columns}
}

func AddFlag(cmd *cobra.Command, options ...Option) {
	var (
		names  []string
		values []string
	)
	for _, option := range options {
		name := option.Name
		if option.Values != nil {
			name += "=..."
			values = append(values, option.Values...)
		}
		names = append(names, name)
	}
	cmd.Flags().StringArrayP(
		flagName,
		"o",
		[]string{},
		fmt.Sprintf("output options: %s", strings.Join(names, "|")),
	)
	_ = cmd.RegisterFlagCompletionFunc(flagName, cmpl.SuggestCandidates(values...))
	cmd.PreRunE = util.ChainRunE(cmd.PreRunE, validateOutputFlag(options))
}

func validateOutputFlag(options []Option) func(cmd *cobra.Command, args []string) error {
	return func(cmd *cobra.Command, _ []string) error {
		validOptions := map[string]map[string]bool{}
		for _, option := range options {
			if option.Values == nil {
				// all values allowed
				validOptions[option.Name] = nil
			} else {
				// only discrete values allowed
				validOptions[option.Name] = map[string]bool{}
				for _, value := range option.Values {
					validOptions[option.Name][value] = true
				}
			}
		}

		flagValues, err := cmd.Flags().GetStringArray(flagName)
		if err != nil {
			return err
		}

		for _, flagValue := range flagValues {
			parts := strings.SplitN(flagValue, "=", 2)
			if strings.HasPrefix(parts[0], "columns") && len(parts) != 2 {
				return fmt.Errorf("invalid output option format")
			}
			if _, ok := validOptions[parts[0]]; !ok {
				return fmt.Errorf("invalid output option: %s", parts[0])
			}
			if len(parts) > 1 && validOptions[parts[0]] != nil {
				parts[1] = strings.ToLower(strings.TrimSpace(parts[1]))
				for v := range strings.SplitSeq(parts[1], ",") {
					if !validOptions[parts[0]][v] {
						return fmt.Errorf("invalid value for output option %s: %s", parts[0], v)
					}
				}
			}
		}
		return nil
	}
}

func FlagsForCommand(cmd *cobra.Command) Opts {
	opts, _ := cmd.Flags().GetStringArray(flagName)
	return parseOutputFlags(opts)
}

type Opts map[string][]string

// Set sets the key to value. It replaces any existing
// values.
func (o Opts) Set(key, value string) {
	o[key] = []string{value}
}

// Add adds the value to key. It appends to any existing
// values associated with key.
func (o Opts) Add(key, value string) {
	o[key] = append(o[key], value)
}

func (o Opts) IsSet(key string) bool {
	if values, ok := o[key]; ok && len(values) > 0 {
		return true
	}
	return false
}

func parseOutputFlags(in []string) Opts {
	o := Opts{}
	for _, param := range in {
		parts := strings.SplitN(param, "=", 2)
		if len(parts) == 2 {
			o[parts[0]] = strings.Split(parts[1], ",")
		} else {
			o[parts[0]] = []string{""}
		}
	}
	return o
}

// NewTable creates a new Table.
func NewTable[T any](out io.Writer) *Table[T] {
	style := table.Style{
		Name:    "minimal",
		Box:     table.StyleBoxLight,
		Color:   table.ColorOptionsDefault,
		Format:  table.FormatOptionsDefault,
		HTML:    table.DefaultHTMLOptions,
		Options: table.OptionsNoBordersAndSeparators,
		Size:    table.SizeOptionsDefault,
		Title:   table.TitleOptionsDefault,
	}
	style.Box.MiddleVertical = "   "
	style.Box.PaddingLeft = ""
	style.Box.PaddingRight = ""
	style.Options.SeparateColumns = true

	w := table.NewWriter()
	w.SetStyle(style)
	w.SuppressTrailingSpaces()

	return &Table[T]{
		out:           out,
		w:             w,
		columns:       map[string]bool{},
		fieldMapping:  map[string]FieldFn[T]{},
		fieldAlias:    map[string]string{},
		allowedFields: map[string]bool{},
		deprecations:  map[string]string{},
	}
}

type FieldFn[T any] func(obj T) string

// Table is a generic way to format object as a table.
type Table[T any] struct {
	out           io.Writer
	w             table.Writer
	columns       map[string]bool
	fieldMapping  map[string]FieldFn[T]
	fieldAlias    map[string]string
	allowedFields map[string]bool
	deprecations  map[string]string
}

// Columns returns a list of known output columns.
func (o *Table[T]) Columns() (cols []string) {
	for c := range o.columns {
		cols = append(cols, c)
	}
	sort.Strings(cols)
	return
}

// AddFieldAlias overrides the field name to allow custom column headers.
func (o *Table[T]) AddFieldAlias(field, alias string) *Table[T] {
	o.fieldAlias[field] = alias
	return o
}

// AddFieldFn adds a function which handles the output of the specified field.
func (o *Table[T]) AddFieldFn(field string, fn FieldFn[T]) *Table[T] {
	o.fieldMapping[field] = fn
	o.allowedFields[field] = true
	o.columns[field] = true
	return o
}

// MarkFieldAsDeprecated marks the specified field as deprecated. The message will be printed
// to stderr if the column is used.
func (o *Table[T]) MarkFieldAsDeprecated(field string, message string) *Table[T] {
	o.deprecations[field] = message
	return o
}

// AddAllowedFields reads all first level fieldnames of the struct and allows them to be used.
func (o *Table[T]) AddAllowedFields(obj T) *Table[T] {
	v := reflect.ValueOf(obj)
	if v.Kind() == reflect.Pointer {
		v = v.Elem()
	}
	if v.Kind() != reflect.Struct {
		panic("AddAllowedFields input must be a struct or a pointer to a struct")
	}
	t := v.Type()
	for i := 0; i < v.NumField(); i++ {
		k := t.Field(i).Type.Kind()
		if k != reflect.Bool &&
			k != reflect.Float32 &&
			k != reflect.Float64 &&
			k != reflect.String &&
			k != reflect.Int &&
			k != reflect.Int64 {
			// only allow simple values
			// complex values need to be mapped via a FieldFn
			continue
		}
		o.allowedFields[strings.ToLower(t.Field(i).Name)] = true
		o.allowedFields[fieldName(t.Field(i).Name)] = true
		o.columns[fieldName(t.Field(i).Name)] = true
	}
	return o
}

// RemoveAllowedField removes fields from the allowed list.
func (o *Table[T]) RemoveAllowedField(fields ...string) *Table[T] {
	for _, field := range fields {
		delete(o.allowedFields, field)
		delete(o.columns, field)
	}
	return o
}

// ValidateColumns returns a list of warnings for the used columns and an error if invalid columns are specified.
func (o *Table[T]) ValidateColumns(cols []string) ([]string, error) {
	var warnings, invalidCols []string
	for _, col := range cols {
		if warning, isDeprecated := o.deprecations[strings.ToLower(col)]; isDeprecated {
			warnings = append(warnings, warning)
		}
		if _, ok := o.allowedFields[strings.ToLower(col)]; !ok {
			invalidCols = append(invalidCols, col)
		}
	}
	if len(invalidCols) > 0 {
		return warnings, fmt.Errorf("invalid table columns: %s", strings.Join(invalidCols, ","))
	}
	return warnings, nil
}

// WriteHeader writes the table header.
func (o *Table[T]) WriteHeader(columns []string) {
	header := table.Row{}
	for _, col := range columns {
		if alias, ok := o.fieldAlias[col]; ok {
			col = alias
		}

		header = append(header, strings.ReplaceAll(strings.ToUpper(col), "_", " "))
	}
	o.w.AppendHeader(header)
}

func (o *Table[T]) Flush() error {
	_, _ = o.out.Write([]byte(o.w.Render()))
	_, _ = o.out.Write([]byte("\n"))
	return nil
}

// Write writes a table line.
func (o *Table[T]) Write(columns []string, obj T) {
	data := structs.Map(obj)
	dataL := map[string]any{}
	for key, value := range data {
		dataL[strings.ToLower(key)] = value
	}

	var out table.Row
	for _, col := range columns {
		colName := strings.ToLower(col)
		if alias, ok := o.fieldAlias[colName]; ok {
			if fn, ok := o.fieldMapping[alias]; ok {
				out = append(out, fn(obj))
				continue
			}
		}
		if fn, ok := o.fieldMapping[colName]; ok {
			out = append(out, fn(obj))
			continue
		}
		if value, ok := dataL[strings.ReplaceAll(colName, "_", "")]; ok {
			if value == nil {
				out = append(out, util.NA(""))
				continue
			}
			if b, ok := value.(bool); ok {
				out = append(out, util.YesNo(b))
				continue
			}
			if s, ok := value.(string); ok {
				out = append(out, util.NA(s))
				continue
			}
			out = append(out, fmt.Sprintf("%v", value))
		}
	}
	o.w.AppendRow(out)
}

func (o *Table[T]) ToAny() *Table[any] {
	fieldMapping := make(map[string]FieldFn[any])
	for k, v := range o.fieldMapping {
		fieldMapping[k] = func(obj any) string {
			if castedObj, ok := obj.(T); ok {
				return v(castedObj)
			}
			return ""
		}
	}
	return &Table[any]{
		out:           o.out,
		w:             o.w,
		columns:       o.columns,
		fieldMapping:  fieldMapping,
		fieldAlias:    o.fieldAlias,
		allowedFields: o.allowedFields,
	}
}

func fieldName(name string) string {
	r := []rune(name)
	var out []rune
	for i := range r {
		if i > 0 && (unicode.IsUpper(r[i])) && (i+1 < len(r) && unicode.IsLower(r[i+1]) || unicode.IsLower(r[i-1])) {
			out = append(out, '_')
		}
		out = append(out, unicode.ToLower(r[i]))
	}
	return string(out)
}
