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

package views

import (
	"maps"

	"github.com/hashicorp/hcl/v2"
	"github.com/mitchellh/colorstring"
	"github.com/opentofu/opentofu/internal/command/arguments"
	"github.com/opentofu/opentofu/internal/command/format"
	"github.com/opentofu/opentofu/internal/terminal"
	"github.com/opentofu/opentofu/internal/tfdiags"
	"github.com/opentofu/opentofu/internal/tofu"
)

// View is the base layer for command views, encapsulating a set of I/O
// streams, a colorize implementation, and implementing a human friendly view
// for diagnostics.
type View struct {
	streams  *terminal.Streams
	colorize *colorstring.Colorize

	compactWarnings     bool
	consolidateWarnings bool
	consolidateErrors   bool

	// When this is true it's a hint that OpenTofu is being run indirectly
	// via a wrapper script or other automation and so we may wish to replace
	// direct examples of commands to run with more conceptual directions.
	// However, we only do this on a best-effort basis, typically prioritizing
	// the messages that users are most likely to see.
	runningInAutomation bool

	// Concise is used to reduce the level of noise in the output and display
	// only the important details.
	concise bool

	// ModuleDeprecationWarnLvl is used to filter out deprecation warnings for outputs and variables as requested by the user.
	ModuleDeprecationWarnLvl tofu.DeprecationWarningLevel

	// showSensitive is used to display the value of variables marked as sensitive.
	showSensitive bool

	// Because some commands used before the UI to print diagnostics, those were printed using an [*ln] function, so
	// we want to be able to configure this for some of the commands to be able to keep the behavior consistent.
	diagsPrinter func(severity tfdiags.Severity, msg string)

	// This unfortunate wart is required to enable rendering of diagnostics which
	// have associated source code in the configuration. This function pointer
	// will be dereferenced as late as possible when rendering diagnostics in
	// order to access the config loader cache.
	configSources func() map[string]*hcl.File
}

// Initialize a View with the given streams, a disabled colorize object, and a
// no-op configSources callback.
func NewView(streams *terminal.Streams) *View {
	return &View{
		streams: streams,
		colorize: &colorstring.Colorize{
			Colors:  colorstring.DefaultColors,
			Disable: true,
			Reset:   true,
		},
		configSources: func() map[string]*hcl.File { return nil },
		diagsPrinter: func(severity tfdiags.Severity, msg string) {
			if severity == tfdiags.Error {
				_, _ = streams.Eprint(msg)
			} else {
				_, _ = streams.Print(msg)
			}
		},
	}
}

// SetRunningInAutomation modifies the view's "running in automation" flag,
// which causes some slight adjustments to certain messages that would normally
// suggest specific OpenTofu commands to run, to make more conceptual gestures
// instead for situations where the user isn't running OpenTofu directly.
//
// For convenient use during initialization (in conjunction with NewView),
// SetRunningInAutomation returns the receiver after modifying it.
func (v *View) SetRunningInAutomation(new bool) *View {
	v.runningInAutomation = new
	return v
}

func (v *View) RunningInAutomation() bool {
	return v.runningInAutomation
}

// Configure applies the global view configuration flags.
func (v *View) Configure(view *arguments.View) {
	colors := maps.Clone(colorstring.DefaultColors)
	colors["purple"] = "38;5;57" // Add also purple to the colorise colors set

	v.colorize.Disable = view.NoColor
	v.colorize.Colors = colors
	v.compactWarnings = view.CompactWarnings
	v.consolidateWarnings = view.ConsolidateWarnings
	v.consolidateErrors = view.ConsolidateErrors
	v.concise = view.Concise
	v.ModuleDeprecationWarnLvl = view.ModuleDeprecationWarnLvl
}

func (v *View) DiagsWithNewline() {
	v.diagsPrinter = func(severity tfdiags.Severity, msg string) {
		if severity == tfdiags.Error {
			_, _ = v.streams.Eprintln(msg)
		} else {
			_, _ = v.streams.Println(msg)
		}
	}
}

// SetConfigSources overrides the default no-op callback with a new function
// pointer, and should be called when the config loader is initialized.
func (v *View) SetConfigSources(cb func() map[string]*hcl.File) {
	v.configSources = cb
}

// Diagnostics renders a set of warnings and errors in human-readable form.
// Warnings are printed to stdout, and errors to stderr.
func (v *View) Diagnostics(diags tfdiags.Diagnostics) {
	diags.Sort()

	if len(diags) == 0 {
		return
	}

	// Filter the deprecation warnings based on the cli arg.
	var newDiags tfdiags.Diagnostics
	seen := tofu.DeprecationDiagnosticAllowedSeen{}
	for _, diag := range diags {
		if !tofu.DeprecationDiagnosticAllowed(v.ModuleDeprecationWarnLvl, diag, seen) {
			continue
		}
		newDiags = append(newDiags, diag)
	}
	diags = newDiags

	if v.consolidateWarnings {
		diags = diags.Consolidate(1, tfdiags.Warning)
	}
	if v.consolidateErrors {
		diags = diags.Consolidate(1, tfdiags.Error)
	}

	// Since warning messages are generally competing
	if v.compactWarnings {
		// If the user selected compact warnings and all of the diagnostics are
		// warnings then we'll use a more compact representation of the warnings
		// that only includes their summaries.
		// We show full warnings if there are also errors, because a warning
		// can sometimes serve as good context for a subsequent error.
		useCompact := true
		for _, diag := range diags {
			if diag.Severity() != tfdiags.Warning {
				useCompact = false
				break
			}
		}
		if useCompact {
			msg := format.DiagnosticWarningsCompact(diags, v.colorize)
			msg = "\n" + msg + "\nTo see the full warning notes, run OpenTofu without -compact-warnings.\n"
			v.streams.Print(msg)
			return
		}
	}

	for _, diag := range diags {
		var msg string
		if v.colorize.Disable {
			msg = format.DiagnosticPlain(diag, v.configSources(), v.streams.Stderr.Columns())
		} else {
			msg = format.Diagnostic(diag, v.configSources(), v.colorize, v.streams.Stderr.Columns())
		}

		// TODO meta-refactor: once we are done with migrating all the commands to views, we should get rid
		// of the check and just allow the diagsPrinter to be called directly.
		if v.diagsPrinter != nil {
			v.diagsPrinter(diag.Severity(), msg)
			continue
		}
		if diag.Severity() == tfdiags.Error {
			v.streams.Eprint(msg)
		} else {
			v.streams.Print(msg)
		}
	}
}

// HelpPrompt is intended to be called from commands which fail to parse all
// of their CLI arguments successfully. It refers users to the full help output
// rather than rendering it directly, which can be overwhelming and confusing.
func (v *View) HelpPrompt(command string) {
	v.streams.Eprintf(helpPrompt, command)
}

const helpPrompt = `
For more help on using this command, run:
  tofu %s -help
`

// outputColumns returns the number of text character cells any non-error
// output should be wrapped to.
//
// This is the number of columns to use if you are calling v.streams.Print or
// related functions.
func (v *View) outputColumns() int {
	return v.streams.Stdout.Columns()
}

// errorColumns returns the number of text character cells any error
// output should be wrapped to.
//
// This is the number of columns to use if you are calling v.streams.Eprint
// or related functions.
func (v *View) errorColumns() int {
	return v.streams.Stderr.Columns()
}

// outputHorizRule will call v.streams.Println with enough horizontal line
// characters to fill an entire row of output.
//
// If UI color is enabled, the rule will get a dark grey coloring to try to
// visually de-emphasize it.
func (v *View) outputHorizRule() {
	v.streams.Println(format.HorizontalRule(v.colorize, v.outputColumns()))
}

func (v *View) SetShowSensitive(showSensitive bool) {
	v.showSensitive = showSensitive
}

// Colorize returns the [colorstring.Colorize] object within to be used in other places.
// TODO meta-refactor: this is a temporary solution. This should not be exposed. Whoever needs to use this
//
//	should do it through a View implementation instead.
func (v *View) Colorize() *colorstring.Colorize {
	return v.colorize
}

// StdinPiped returns true if the input is piped.
func (v *View) StdinPiped() bool {
	return !v.streams.Stdin.IsTerminal()
}
