// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1

package views

import (
	encJson "encoding/json"
	"fmt"

	"github.com/hashicorp/go-hclog"
	"github.com/hashicorp/hcl/v2"

	"github.com/hashicorp/terraform/internal/command/views/json"
	"github.com/hashicorp/terraform/internal/policy"
	"github.com/hashicorp/terraform/internal/tfdiags"
	tfversion "github.com/hashicorp/terraform/version"
)

// This version describes the schema of JSON UI messages. This version must be
// updated after making any changes to this view, the jsonHook, or any of the
// command/views/json package.
const JSON_UI_VERSION = "1.3"

func NewJSONView(view *View) *JSONView {
	log := hclog.New(&hclog.LoggerOptions{
		Name:       "terraform.ui",
		Output:     view.streams.Stdout.File,
		JSONFormat: true,
	})
	jv := &JSONView{
		log:  log,
		view: view,
	}
	jv.Version()
	return jv
}

type JSONView struct {
	// hclog is used for all output in JSON UI mode. The logger has an internal
	// mutex to ensure that messages are not interleaved.
	log hclog.Logger

	// We hold a reference to the view entirely to allow us to access the
	// ConfigSources function pointer, in order to render source snippets into
	// diagnostics. This is even more unfortunate than the same reference in the
	// view.
	//
	// Do not be tempted to dereference the configSource value upon logger init,
	// as it will likely be updated later.
	view *View
}

func (v *JSONView) Version() {
	version := tfversion.String()
	v.log.Info(
		fmt.Sprintf("Terraform %s", version),
		"type", json.MessageVersion,
		"terraform", version,
		"ui", JSON_UI_VERSION,
	)
}

func (v *JSONView) Log(message string) {
	v.log.Info(message, "type", json.MessageLog)
}

func (v *JSONView) StateDump(state string) {
	v.log.Info(
		"Emergency state dump",
		"type", json.MessageLog,
		"state", encJson.RawMessage(state),
	)
}

func (v *JSONView) Diagnostics(diags tfdiags.Diagnostics, metadata ...interface{}) {
	sources := v.view.configSources()
	for _, diag := range diags {
		diagnostic := json.NewDiagnostic(diag, sources)

		args := []interface{}{"type", json.MessageDiagnostic, "diagnostic", diagnostic}
		args = append(args, metadata...)

		switch diag.Severity() {
		case tfdiags.Warning:
			v.log.Warn(fmt.Sprintf("Warning: %s", diag.Description().Summary), args...)
		default:
			v.log.Error(fmt.Sprintf("Error: %s", diag.Description().Summary), args...)
		}
	}
}

func (v *JSONView) PlannedChange(c *json.ResourceInstanceChange) {
	v.log.Info(
		c.String(),
		"type", json.MessagePlannedChange,
		"change", c,
	)
}

func (v *JSONView) PlannedActionInvocation(action *json.ActionInvocation) {
	v.log.Info(
		fmt.Sprintf("planned action invocation: %s", action.Action.Action),
		"type", json.MessagePlannedActionInvocation,
		"invocation", action,
	)
}

func (v *JSONView) AppliedActionInvocation(action *json.ActionInvocation) {
	v.log.Info(
		fmt.Sprintf("applied action invocation: %s", action.Action.Action),
		"type", json.MessageAppliedActionInvocation,
		"invocation", action,
	)
}

func (v *JSONView) ResourceDrift(c *json.ResourceInstanceChange) {
	v.log.Info(
		fmt.Sprintf("%s: Drift detected (%s)", c.Resource.Addr, c.Action),
		"type", json.MessageResourceDrift,
		"change", c,
	)
}

func (v *JSONView) ChangeSummary(cs *json.ChangeSummary) {
	v.log.Info(
		cs.String(),
		"type", json.MessageChangeSummary,
		"changes", cs,
	)
}

func (v *JSONView) Hook(h json.Hook) {
	v.log.Info(
		h.String(),
		"type", h.HookType(),
		"hook", h,
	)
}

func (v *JSONView) Outputs(outputs json.Outputs) {
	v.log.Info(
		outputs.String(),
		"type", json.MessageOutputs,
		"outputs", outputs,
	)
}

func (v *JSONView) logPolicyResult(addr string, resp policy.EvaluationResponse) {
	// Log all the info messages
	for _, enforcement := range resp.Enforcements {
		if enforcement.Message == "" {
			continue
		}
		var src []byte
		if enforcement.LocalRange != nil {
			src = v.view.configSources()[enforcement.LocalRange.Filename]
		}
		info := json.NewPolicyInfo(src, enforcement)
		args := []any{
			"type", json.MessagePolicyInfo,
			"target_address", addr,
			json.MessagePolicyInfo, info,
			"@policy", "true",
			"result", enforcement.Result.String(),
		}
		if enforcement.Policy != nil {
			args = append(args, "policy_metadata", json.MetadataFromEnforcement(enforcement))
		}
		v.log.Info("Policy info", args...)
	}

	for _, diag := range resp.Diagnostics {
		v.logPolicyDiagnostic(diag, "target_address", addr)
	}

	for _, policy := range resp.Policies {
		v.log.Info(
			"Policy Result",
			"type", json.MessagePolicyEvaluationResult,
			"result", policy.Result.String(),
			"target_address", addr,
			"policy_address", policy.Address,
			"@policy", "true",
			"policy_metadata", json.MetadataFromPolicy(*policy),
		)
	}
}

func (v *JSONView) PolicyResult(addr string, resp policy.EvaluationResponse) {
	v.logPolicyResult(addr, resp)
}

// PolicyDiagnostics logs policy diagnostics that are not tied to a specific
// target, such as setup diagnostics (e.g. a failure to connect to the policy
// engine), so no target address is attached.
func (v *JSONView) PolicyDiagnostics(diags policy.Diagnostics) {
	for _, diag := range diags {
		v.logPolicyDiagnostic(diag)
	}
}

// logPolicyDiagnostic logs the policy diagnostics. These are emitted as `policy_diagnostic` messages,
// not standard Terraform `diagnostic` messages. We use the severity level only to
// set the log level for that policy-specific output.
func (v *JSONView) logPolicyDiagnostic(diag tfdiags.Diagnostic, extraArgs ...any) {
	sources := v.view.configSources()
	diagnostic := json.NewDiagnostic(diag, sources)

	args := []any{
		"type", json.MessagePolicyDiagnostic,
		"@policy", "true",
		json.MessagePolicyDiagnostic, diagnostic,
	}
	args = append(args, extraArgs...)
	extra := tfdiags.ExtraInfo[*policy.PolicyExtra](diag)
	if extra != nil {
		policyMetadata := json.MetadataFromPolicy(extra.Policy)
		if extra.EnforceIndex != nil {
			policyMetadata.EnforceIndex = extra.EnforceIndex
		}
		args = append(args, "policy_metadata", policyMetadata)
		args = append(args, "result", extra.Result.String())
	}
	switch extra.Severity {
	case hcl.DiagWarning:
		v.log.Warn(fmt.Sprintf("Warning: %s", diag.Description().Summary), args...)
	default:
		v.log.Error(fmt.Sprintf("Error: %s", diag.Description().Summary), args...)
	}
}

var _ Spacer = (*JSONView)(nil)

// Spacer is a no-op for JSON view, as empty lines are not needed to space-out logs in machine-readable output.
func (v *JSONView) Spacer() {
	// do nothing
}
