// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package renderers

import (
	"bytes"
	"fmt"

	"github.com/opentofu/opentofu/internal/command/jsonformat/computed"
	"github.com/opentofu/opentofu/internal/plans"
)

var _ computed.DiffRenderer = (*listRenderer)(nil)

func List(elements []computed.Diff) computed.DiffRenderer {
	return &listRenderer{
		displayContext: true,
		elements:       elements,
	}
}

func NestedList(elements []computed.Diff) computed.DiffRenderer {
	return &listRenderer{
		elements: elements,
	}
}

type listRenderer struct {
	NoWarningsRenderer

	displayContext bool
	elements       []computed.Diff
}

func (renderer listRenderer) RenderHuman(diff computed.Diff, indent int, opts computed.RenderHumanOpts) string {
	if len(renderer.elements) == 0 {
		return fmt.Sprintf("[]%s%s", nullSuffix(diff.Action, opts), forcesReplacement(diff.Replace, opts))
	}

	elementOpts := opts.Clone()
	elementOpts.OverrideNullSuffix = true

	unchangedElementOpts := opts.Clone()
	unchangedElementOpts.ShowUnchangedChildren = true

	var unchangedElements []computed.Diff

	// renderNext tells the renderer to print out the next element in the list
	// whatever state it is in. So, even if a change is a NoOp we will still
	// print it out if the last change we processed wants us to.
	renderNext := false

	var buf bytes.Buffer
	buf.WriteString(fmt.Sprintf("[%s\n", forcesReplacement(diff.Replace, opts)))
	for _, element := range renderer.elements {
		if element.Action == plans.NoOp && !renderNext && !opts.ShowUnchangedChildren {
			unchangedElements = append(unchangedElements, element)
			continue
		}
		renderNext = false

		opts := elementOpts

		// If we want to display the context around this change, we want to
		// render the change immediately before this change in the list, and the
		// change immediately after in the list, even if both these changes are
		// NoOps. This will give the user reading the diff some context as to
		// where in the list these changes are being made, as order matters.
		if renderer.displayContext {
			// If our list of unchanged elements contains more than one entry
			// we'll print out a count of the number of unchanged elements that
			// we skipped. Note, this is the length of the unchanged elements
			// minus 1 as the most recent unchanged element will be printed out
			// in full.
			if len(unchangedElements) > 1 {
				buf.WriteString(fmt.Sprintf("%s%s%s\n", formatIndent(indent+1), writeDiffActionSymbol(plans.NoOp, opts), unchanged("element", len(unchangedElements)-1, opts)))
			}
			// If our list of unchanged elements contains at least one entry,
			// we're going to print out the most recent change in full. That's
			// what happens here.
			if len(unchangedElements) > 0 {
				lastElement := unchangedElements[len(unchangedElements)-1]
				buf.WriteString(fmt.Sprintf("%s%s%s,\n", formatIndent(indent+1), writeDiffActionSymbol(lastElement.Action, unchangedElementOpts), lastElement.RenderHuman(indent+1, unchangedElementOpts)))
			}
			// We now reset the unchanged elements list, we've printed out a
			// count of all the elements we skipped so we start counting from
			// scratch again. This means that if we process a run of changed
			// elements, they won't all start printing out summaries of every
			// change that happened previously.
			unchangedElements = nil

			if element.Action == plans.NoOp {
				// If this is a NoOp action then we're going to render it below
				// so we need to just override the opts we're going to use to
				// make sure we use the unchanged opts.
				opts = unchangedElementOpts
			} else {
				// As we also want to render the element immediately after any
				// changes, we make a note here to say we should render the next
				// change whatever it is. But, we only want to render the next
				// change if the current change isn't a NoOp. If the current change
				// is a NoOp then it was told to print by the last change and we
				// don't want to cascade and print all changes from now on.
				renderNext = true
			}
		}

		for _, warning := range element.WarningsHuman(indent+1, opts) {
			buf.WriteString(fmt.Sprintf("%s%s\n", formatIndent(indent+1), warning))
		}
		buf.WriteString(fmt.Sprintf("%s%s%s,\n", formatIndent(indent+1), writeDiffActionSymbol(element.Action, opts), element.RenderHuman(indent+1, opts)))
	}

	// If we were not displaying any context alongside our changes then the
	// unchangedElements list will contain every unchanged element, and we'll
	// print that out as we do with every other collection.
	//
	// If we were displaying context, then this will contain any unchanged
	// elements since our last change, so we should also print it out.
	if len(unchangedElements) > 0 {
		buf.WriteString(fmt.Sprintf("%s%s%s\n", formatIndent(indent+1), writeDiffActionSymbol(plans.NoOp, opts), unchanged("element", len(unchangedElements), opts)))
	}

	buf.WriteString(fmt.Sprintf("%s%s]%s", formatIndent(indent), writeDiffActionSymbol(plans.NoOp, opts), nullSuffix(diff.Action, opts)))
	return buf.String()
}
