// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.19

package inspect

import (
	"bytes"
	"encoding/json"
	"io"
	"strings"
	"text/template"

	"github.com/docker/cli/cli"
	"github.com/docker/cli/templates"
	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"
)

// Inspector defines an interface to implement to process elements
type Inspector interface {
	// Inspect writes the raw element in JSON format.
	Inspect(typedElement any, rawElement []byte) error
	// Flush writes the result of inspecting all elements into the output stream.
	Flush() error
}

// TemplateInspector uses a text template to inspect elements.
type TemplateInspector struct {
	outputStream io.Writer
	buffer       *bytes.Buffer
	tmpl         *template.Template
}

// NewTemplateInspector creates a new inspector with a template.
func NewTemplateInspector(outputStream io.Writer, tmpl *template.Template) Inspector {
	return &TemplateInspector{
		outputStream: outputStream,
		buffer:       new(bytes.Buffer),
		tmpl:         tmpl,
	}
}

// NewTemplateInspectorFromString creates a new TemplateInspector from a string
// which is compiled into a template.
func NewTemplateInspectorFromString(out io.Writer, tmplStr string) (Inspector, error) {
	if tmplStr == "" {
		return NewIndentedInspector(out), nil
	}

	if tmplStr == "json" {
		return NewJSONInspector(out), nil
	}

	tmpl, err := templates.Parse(tmplStr)
	if err != nil {
		return nil, errors.Errorf("template parsing error: %s", err)
	}
	return NewTemplateInspector(out, tmpl), nil
}

// GetRefFunc is a function which used by Inspect to fetch an object from a
// reference
type GetRefFunc func(ref string) (any, []byte, error)

// Inspect fetches objects by reference using GetRefFunc and writes the json
// representation to the output writer.
func Inspect(out io.Writer, references []string, tmplStr string, getRef GetRefFunc) error {
	inspector, err := NewTemplateInspectorFromString(out, tmplStr)
	if err != nil {
		return cli.StatusError{StatusCode: 64, Status: err.Error()}
	}

	var inspectErrs []string
	for _, ref := range references {
		element, raw, err := getRef(ref)
		if err != nil {
			inspectErrs = append(inspectErrs, err.Error())
			continue
		}

		if err := inspector.Inspect(element, raw); err != nil {
			inspectErrs = append(inspectErrs, err.Error())
		}
	}

	if err := inspector.Flush(); err != nil {
		logrus.Errorf("%s\n", err)
	}

	if len(inspectErrs) != 0 {
		return cli.StatusError{
			StatusCode: 1,
			Status:     strings.Join(inspectErrs, "\n"),
		}
	}
	return nil
}

// Inspect executes the inspect template.
// It decodes the raw element into a map if the initial execution fails.
// This allows docker cli to parse inspect structs injected with Swarm fields.
func (i *TemplateInspector) Inspect(typedElement any, rawElement []byte) error {
	buffer := new(bytes.Buffer)
	if err := i.tmpl.Execute(buffer, typedElement); err != nil {
		if rawElement == nil {
			return errors.Errorf("template parsing error: %v", err)
		}
		return i.tryRawInspectFallback(rawElement)
	}
	i.buffer.Write(buffer.Bytes())
	i.buffer.WriteByte('\n')
	return nil
}

// tryRawInspectFallback executes the inspect template with a raw interface.
// This allows docker cli to parse inspect structs injected with Swarm fields.
func (i *TemplateInspector) tryRawInspectFallback(rawElement []byte) error {
	var raw any
	buffer := new(bytes.Buffer)
	rdr := bytes.NewReader(rawElement)
	dec := json.NewDecoder(rdr)
	dec.UseNumber()

	if rawErr := dec.Decode(&raw); rawErr != nil {
		return errors.Errorf("unable to read inspect data: %v", rawErr)
	}

	tmplMissingKey := i.tmpl.Option("missingkey=error")
	if rawErr := tmplMissingKey.Execute(buffer, raw); rawErr != nil {
		return errors.Errorf("template parsing error: %v", rawErr)
	}

	i.buffer.Write(buffer.Bytes())
	i.buffer.WriteByte('\n')
	return nil
}

// Flush writes the result of inspecting all elements into the output stream.
func (i *TemplateInspector) Flush() error {
	if i.buffer.Len() == 0 {
		_, err := io.WriteString(i.outputStream, "\n")
		return err
	}
	_, err := io.Copy(i.outputStream, i.buffer)
	return err
}

// NewIndentedInspector generates a new inspector with an indented representation
// of elements.
func NewIndentedInspector(outputStream io.Writer) Inspector {
	return &elementsInspector{
		outputStream: outputStream,
		raw: func(dst *bytes.Buffer, src []byte) error {
			return json.Indent(dst, src, "", "    ")
		},
		el: func(v any) ([]byte, error) {
			return json.MarshalIndent(v, "", "    ")
		},
	}
}

// NewJSONInspector generates a new inspector with a compact representation
// of elements.
func NewJSONInspector(outputStream io.Writer) Inspector {
	return &elementsInspector{
		outputStream: outputStream,
		raw:          json.Compact,
		el:           json.Marshal,
	}
}

type elementsInspector struct {
	outputStream io.Writer
	elements     []any
	rawElements  [][]byte
	raw          func(dst *bytes.Buffer, src []byte) error
	el           func(v any) ([]byte, error)
}

func (e *elementsInspector) Inspect(typedElement any, rawElement []byte) error {
	if rawElement != nil {
		e.rawElements = append(e.rawElements, rawElement)
	} else {
		e.elements = append(e.elements, typedElement)
	}
	return nil
}

func (e *elementsInspector) Flush() error {
	if len(e.elements) == 0 && len(e.rawElements) == 0 {
		_, err := io.WriteString(e.outputStream, "[]\n")
		return err
	}

	var buffer io.Reader
	if len(e.rawElements) > 0 {
		bytesBuffer := new(bytes.Buffer)
		bytesBuffer.WriteString("[")
		for idx, r := range e.rawElements {
			bytesBuffer.Write(r)
			if idx < len(e.rawElements)-1 {
				bytesBuffer.WriteString(",")
			}
		}
		bytesBuffer.WriteString("]")
		output := new(bytes.Buffer)
		if err := e.raw(output, bytesBuffer.Bytes()); err != nil {
			return err
		}
		buffer = output
	} else {
		b, err := e.el(e.elements)
		if err != nil {
			return err
		}
		buffer = bytes.NewReader(b)
	}

	if _, err := io.Copy(e.outputStream, buffer); err != nil {
		return err
	}
	_, err := io.WriteString(e.outputStream, "\n")
	return err
}
