package terraform

import (
	"fmt"
	"log"

	"github.com/hashicorp/hcl/v2"
	"github.com/zclconf/go-cty/cty"

	"github.com/hashicorp/terraform-plugin-sdk/internal/addrs"
	"github.com/hashicorp/terraform-plugin-sdk/internal/plans"
	"github.com/hashicorp/terraform-plugin-sdk/internal/states"
)

// EvalDeleteOutput is an EvalNode implementation that deletes an output
// from the state.
type EvalDeleteOutput struct {
	Addr addrs.OutputValue
}

// TODO: test
func (n *EvalDeleteOutput) Eval(ctx EvalContext) (interface{}, error) {
	state := ctx.State()
	if state == nil {
		return nil, nil
	}

	state.RemoveOutputValue(n.Addr.Absolute(ctx.Path()))
	return nil, nil
}

// EvalWriteOutput is an EvalNode implementation that writes the output
// for the given name to the current state.
type EvalWriteOutput struct {
	Addr      addrs.OutputValue
	Sensitive bool
	Expr      hcl.Expression
	// ContinueOnErr allows interpolation to fail during Input
	ContinueOnErr bool
}

// TODO: test
func (n *EvalWriteOutput) Eval(ctx EvalContext) (interface{}, error) {
	addr := n.Addr.Absolute(ctx.Path())

	// This has to run before we have a state lock, since evaluation also
	// reads the state
	val, diags := ctx.EvaluateExpr(n.Expr, cty.DynamicPseudoType, nil)
	// We'll handle errors below, after we have loaded the module.

	state := ctx.State()
	if state == nil {
		return nil, nil
	}

	changes := ctx.Changes() // may be nil, if we're not working on a changeset

	// handling the interpolation error
	if diags.HasErrors() {
		if n.ContinueOnErr || flagWarnOutputErrors {
			log.Printf("[ERROR] Output interpolation %q failed: %s", n.Addr.Name, diags.Err())
			// if we're continuing, make sure the output is included, and
			// marked as unknown. If the evaluator was able to find a type
			// for the value in spite of the error then we'll use it.
			n.setValue(addr, state, changes, cty.UnknownVal(val.Type()))
			return nil, EvalEarlyExitError{}
		}
		return nil, diags.Err()
	}

	n.setValue(addr, state, changes, val)

	return nil, nil
}

func (n *EvalWriteOutput) setValue(addr addrs.AbsOutputValue, state *states.SyncState, changes *plans.ChangesSync, val cty.Value) {
	if val.IsKnown() && !val.IsNull() {
		// The state itself doesn't represent unknown values, so we null them
		// out here and then we'll save the real unknown value in the planned
		// changeset below, if we have one on this graph walk.
		log.Printf("[TRACE] EvalWriteOutput: Saving value for %s in state", addr)
		stateVal := cty.UnknownAsNull(val)
		state.SetOutputValue(addr, stateVal, n.Sensitive)
	} else {
		log.Printf("[TRACE] EvalWriteOutput: Removing %s from state (it is now null)", addr)
		state.RemoveOutputValue(addr)
	}

	// If we also have an active changeset then we'll replicate the value in
	// there. This is used in preference to the state where present, since it
	// *is* able to represent unknowns, while the state cannot.
	if changes != nil {
		// For the moment we are not properly tracking changes to output
		// values, and just marking them always as "Create" or "Destroy"
		// actions. A future release will rework the output lifecycle so we
		// can track their changes properly, in a similar way to how we work
		// with resource instances.

		var change *plans.OutputChange
		if !val.IsNull() {
			change = &plans.OutputChange{
				Addr:      addr,
				Sensitive: n.Sensitive,
				Change: plans.Change{
					Action: plans.Create,
					Before: cty.NullVal(cty.DynamicPseudoType),
					After:  val,
				},
			}
		} else {
			change = &plans.OutputChange{
				Addr:      addr,
				Sensitive: n.Sensitive,
				Change: plans.Change{
					// This is just a weird placeholder delete action since
					// we don't have an actual prior value to indicate.
					// FIXME: Generate real planned changes for output values
					// that include the old values.
					Action: plans.Delete,
					Before: cty.NullVal(cty.DynamicPseudoType),
					After:  cty.NullVal(cty.DynamicPseudoType),
				},
			}
		}

		cs, err := change.Encode()
		if err != nil {
			// Should never happen, since we just constructed this right above
			panic(fmt.Sprintf("planned change for %s could not be encoded: %s", addr, err))
		}
		log.Printf("[TRACE] EvalWriteOutput: Saving %s change for %s in changeset", change.Action, addr)
		changes.RemoveOutputChange(addr) // remove any existing planned change, if present
		changes.AppendOutputChange(cs)   // add the new planned change
	}
}
