package formats

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"os"
	"strings"
	"text/tabwriter"
	"text/template"

	terminal "golang.org/x/term"
	"sigs.k8s.io/yaml"
)

const (
	// JSONString const to save on duplicate variable names
	JSONString = "json"
	// IDString const to save on duplicates for Go templates
	IDString = "{{.ID}}"

	parsingErrorStr = "Template parsing error"
)

// Writer interface for outputs
type Writer interface {
	Out() error
}

// JSONStructArray for JSON output
type JSONStructArray struct {
	Output []any
}

// StdoutTemplateArray for Go template output
type StdoutTemplateArray struct {
	Output   []any
	Template string
	Fields   map[string]string
}

// JSONStruct for JSON output
type JSONStruct struct {
	Output any
}

// StdoutTemplate for Go template output
type StdoutTemplate struct {
	Output   any
	Template string
	Fields   map[string]string
}

// YAMLStruct for YAML output
type YAMLStruct struct {
	Output any
}

func setJSONFormatEncoder(isTerminal bool, w io.Writer) *json.Encoder {
	enc := json.NewEncoder(w)
	enc.SetIndent("", "    ")
	if isTerminal {
		enc.SetEscapeHTML(false)
	}
	return enc
}

// Out method for JSON Arrays
func (j JSONStructArray) Out() error {
	buf := bytes.NewBuffer(nil)
	enc := setJSONFormatEncoder(terminal.IsTerminal(int(os.Stdout.Fd())), buf)
	if err := enc.Encode(j.Output); err != nil {
		return err
	}
	data := buf.Bytes()

	// JSON returns a byte array with a literal null [110 117 108 108] in it
	// if it is passed empty data.  We used bytes.Compare to see if that is
	// the case.
	if diff := bytes.Compare(data, []byte("null")); diff == 0 {
		data = []byte("[]")
	}

	// If the we did get NULL back, we should spit out {} which is
	// at least valid JSON for the consumer.
	fmt.Printf("%s", data) //nolint:forbidigo
	humanNewLine()
	return nil
}

// Out method for Go templates
func (t StdoutTemplateArray) Out() error {
	w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
	if strings.HasPrefix(t.Template, "table") {
		// replace any spaces with tabs in template so that tabwriter can align it
		t.Template = strings.ReplaceAll(strings.TrimSpace(t.Template[5:]), " ", "\t")
		headerTmpl, err := template.New("header").Funcs(headerFunctions).Parse(t.Template)
		if err != nil {
			return fmt.Errorf("%s: %w", parsingErrorStr, err)
		}
		err = headerTmpl.Execute(w, t.Fields)
		if err != nil {
			return err
		}
		fmt.Fprintln(w, "")
	}
	t.Template = strings.ReplaceAll(t.Template, " ", "\t")
	tmpl, err := template.New("image").Funcs(basicFunctions).Parse(t.Template)
	if err != nil {
		return fmt.Errorf("%s: %w", parsingErrorStr, err)
	}
	for _, raw := range t.Output {
		basicTmpl := tmpl.Funcs(basicFunctions)
		if err := basicTmpl.Execute(w, raw); err != nil {
			return fmt.Errorf("%s: %w", parsingErrorStr, err)
		}
		fmt.Fprintln(w, "")
	}
	return w.Flush()
}

// Out method for JSON struct
func (j JSONStruct) Out() error {
	data, err := json.MarshalIndent(j.Output, "", "    ")
	if err != nil {
		return err
	}
	fmt.Printf("%s", data) //nolint:forbidigo
	humanNewLine()
	return nil
}

// Out method for Go templates
func (t StdoutTemplate) Out() error {
	tmpl, err := template.New("image").Parse(t.Template)
	if err != nil {
		return fmt.Errorf("template parsing error: %w", err)
	}
	err = tmpl.Execute(os.Stdout, t.Output)
	if err != nil {
		return err
	}
	humanNewLine()
	return nil
}

// Out method for YAML
func (y YAMLStruct) Out() error {
	var buf []byte
	var err error
	buf, err = yaml.Marshal(y.Output)
	if err != nil {
		return err
	}
	fmt.Printf("%s", string(buf)) //nolint:forbidigo
	humanNewLine()
	return nil
}

// humanNewLine prints a new line at the end of the output only if stdout is the terminal
func humanNewLine() {
	if terminal.IsTerminal(int(os.Stdout.Fd())) {
		fmt.Println() //nolint:forbidigo
	}
}
