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

package json

import (
	"fmt"
	"time"

	"github.com/opentofu/opentofu/internal/addrs"
	"github.com/opentofu/opentofu/internal/command/jsonentities"
	"github.com/opentofu/opentofu/internal/plans"
)

type Hook interface {
	HookType() MessageType
	String() string
}

// ApplyStart: triggered by PreApply hook
type applyStart struct {
	Resource   jsonentities.ResourceAddr `json:"resource"`
	Action     jsonentities.ChangeAction `json:"action"`
	IDKey      string                    `json:"id_key,omitempty"`
	IDValue    string                    `json:"id_value,omitempty"`
	actionVerb string
}

var _ Hook = (*applyStart)(nil)

func (h *applyStart) HookType() MessageType {
	return MessageApplyStart
}

func (h *applyStart) String() string {
	var id string
	if h.IDKey != "" && h.IDValue != "" {
		id = fmt.Sprintf(" [%s=%s]", h.IDKey, h.IDValue)
	}
	return fmt.Sprintf("%s: %s...%s", h.Resource.Addr, h.actionVerb, id)
}

func NewApplyStart(addr addrs.AbsResourceInstance, action plans.Action, idKey string, idValue string) Hook {
	hook := &applyStart{
		Resource:   jsonentities.NewResourceAddr(addr),
		Action:     jsonentities.ParseChangeAction(action),
		IDKey:      idKey,
		IDValue:    idValue,
		actionVerb: startActionVerb(action),
	}

	return hook
}

// ApplyProgress: currently triggered by a timer started on PreApply. In
// future, this might also be triggered by provider progress reporting.
type applyProgress struct {
	Resource   jsonentities.ResourceAddr `json:"resource"`
	Action     jsonentities.ChangeAction `json:"action"`
	Elapsed    float64                   `json:"elapsed_seconds"`
	actionVerb string
	elapsed    time.Duration
}

var _ Hook = (*applyProgress)(nil)

func (h *applyProgress) HookType() MessageType {
	return MessageApplyProgress
}

func (h *applyProgress) String() string {
	return fmt.Sprintf("%s: Still %s... [%s elapsed]", h.Resource.Addr, h.actionVerb, h.elapsed)
}

func NewApplyProgress(addr addrs.AbsResourceInstance, action plans.Action, elapsed time.Duration) Hook {
	return &applyProgress{
		Resource:   jsonentities.NewResourceAddr(addr),
		Action:     jsonentities.ParseChangeAction(action),
		Elapsed:    elapsed.Seconds(),
		actionVerb: progressActionVerb(action),
		elapsed:    elapsed,
	}
}

// ApplyComplete: triggered by PostApply hook
type applyComplete struct {
	Resource   jsonentities.ResourceAddr `json:"resource"`
	Action     jsonentities.ChangeAction `json:"action"`
	IDKey      string                    `json:"id_key,omitempty"`
	IDValue    string                    `json:"id_value,omitempty"`
	Elapsed    float64                   `json:"elapsed_seconds"`
	actionNoun string
	elapsed    time.Duration
}

var _ Hook = (*applyComplete)(nil)

func (h *applyComplete) HookType() MessageType {
	return MessageApplyComplete
}

func (h *applyComplete) String() string {
	var id string
	if h.IDKey != "" && h.IDValue != "" {
		id = fmt.Sprintf(" [%s=%s]", h.IDKey, h.IDValue)
	}
	return fmt.Sprintf("%s: %s complete after %s%s", h.Resource.Addr, h.actionNoun, h.elapsed, id)
}

func NewApplyComplete(addr addrs.AbsResourceInstance, action plans.Action, idKey, idValue string, elapsed time.Duration) Hook {
	return &applyComplete{
		Resource:   jsonentities.NewResourceAddr(addr),
		Action:     jsonentities.ParseChangeAction(action),
		IDKey:      idKey,
		IDValue:    idValue,
		Elapsed:    elapsed.Seconds(),
		actionNoun: actionNoun(action),
		elapsed:    elapsed,
	}
}

// ApplyErrored: triggered by PostApply hook on failure. This will be followed
// by diagnostics when the apply finishes.
type applyErrored struct {
	Resource   jsonentities.ResourceAddr `json:"resource"`
	Action     jsonentities.ChangeAction `json:"action"`
	Elapsed    float64                   `json:"elapsed_seconds"`
	actionNoun string
	elapsed    time.Duration
}

var _ Hook = (*applyErrored)(nil)

func (h *applyErrored) HookType() MessageType {
	return MessageApplyErrored
}

func (h *applyErrored) String() string {
	return fmt.Sprintf("%s: %s errored after %s", h.Resource.Addr, h.actionNoun, h.elapsed)
}

func NewApplyErrored(addr addrs.AbsResourceInstance, action plans.Action, elapsed time.Duration) Hook {
	return &applyErrored{
		Resource:   jsonentities.NewResourceAddr(addr),
		Action:     jsonentities.ParseChangeAction(action),
		Elapsed:    elapsed.Seconds(),
		actionNoun: actionNoun(action),
		elapsed:    elapsed,
	}
}

// ProvisionStart: triggered by PreProvisionInstanceStep hook
type provisionStart struct {
	Resource    jsonentities.ResourceAddr `json:"resource"`
	Provisioner string                    `json:"provisioner"`
}

var _ Hook = (*provisionStart)(nil)

func (h *provisionStart) HookType() MessageType {
	return MessageProvisionStart
}

func (h *provisionStart) String() string {
	return fmt.Sprintf("%s: Provisioning with '%s'...", h.Resource.Addr, h.Provisioner)
}

func NewProvisionStart(addr addrs.AbsResourceInstance, provisioner string) Hook {
	return &provisionStart{
		Resource:    jsonentities.NewResourceAddr(addr),
		Provisioner: provisioner,
	}
}

// ProvisionProgress: triggered by ProvisionOutput hook
type provisionProgress struct {
	Resource    jsonentities.ResourceAddr `json:"resource"`
	Provisioner string                    `json:"provisioner"`
	Output      string                    `json:"output"`
}

var _ Hook = (*provisionProgress)(nil)

func (h *provisionProgress) HookType() MessageType {
	return MessageProvisionProgress
}

func (h *provisionProgress) String() string {
	return fmt.Sprintf("%s: (%s): %s", h.Resource.Addr, h.Provisioner, h.Output)
}

func NewProvisionProgress(addr addrs.AbsResourceInstance, provisioner string, output string) Hook {
	return &provisionProgress{
		Resource:    jsonentities.NewResourceAddr(addr),
		Provisioner: provisioner,
		Output:      output,
	}
}

// ProvisionComplete: triggered by PostProvisionInstanceStep hook
type provisionComplete struct {
	Resource    jsonentities.ResourceAddr `json:"resource"`
	Provisioner string                    `json:"provisioner"`
}

var _ Hook = (*provisionComplete)(nil)

func (h *provisionComplete) HookType() MessageType {
	return MessageProvisionComplete
}

func (h *provisionComplete) String() string {
	return fmt.Sprintf("%s: (%s) Provisioning complete", h.Resource.Addr, h.Provisioner)
}

func NewProvisionComplete(addr addrs.AbsResourceInstance, provisioner string) Hook {
	return &provisionComplete{
		Resource:    jsonentities.NewResourceAddr(addr),
		Provisioner: provisioner,
	}
}

// ProvisionErrored: triggered by PostProvisionInstanceStep hook on failure.
// This will be followed by diagnostics when the apply finishes.
type provisionErrored struct {
	Resource    jsonentities.ResourceAddr `json:"resource"`
	Provisioner string                    `json:"provisioner"`
}

var _ Hook = (*provisionErrored)(nil)

func (h *provisionErrored) HookType() MessageType {
	return MessageProvisionErrored
}

func (h *provisionErrored) String() string {
	return fmt.Sprintf("%s: (%s) Provisioning errored", h.Resource.Addr, h.Provisioner)
}

func NewProvisionErrored(addr addrs.AbsResourceInstance, provisioner string) Hook {
	return &provisionErrored{
		Resource:    jsonentities.NewResourceAddr(addr),
		Provisioner: provisioner,
	}
}

// RefreshStart: triggered by PreRefresh hook
type refreshStart struct {
	Resource jsonentities.ResourceAddr `json:"resource"`
	IDKey    string                    `json:"id_key,omitempty"`
	IDValue  string                    `json:"id_value,omitempty"`
}

var _ Hook = (*refreshStart)(nil)

func (h *refreshStart) HookType() MessageType {
	return MessageRefreshStart
}

func (h *refreshStart) String() string {
	var id string
	if h.IDKey != "" && h.IDValue != "" {
		id = fmt.Sprintf(" [%s=%s]", h.IDKey, h.IDValue)
	}
	return fmt.Sprintf("%s: Refreshing state...%s", h.Resource.Addr, id)
}

func NewRefreshStart(addr addrs.AbsResourceInstance, idKey, idValue string) Hook {
	return &refreshStart{
		Resource: jsonentities.NewResourceAddr(addr),
		IDKey:    idKey,
		IDValue:  idValue,
	}
}

// RefreshComplete: triggered by PostRefresh hook
type refreshComplete struct {
	Resource jsonentities.ResourceAddr `json:"resource"`
	IDKey    string                    `json:"id_key,omitempty"`
	IDValue  string                    `json:"id_value,omitempty"`
}

var _ Hook = (*refreshComplete)(nil)

func (h *refreshComplete) HookType() MessageType {
	return MessageRefreshComplete
}

func (h *refreshComplete) String() string {
	var id string
	if h.IDKey != "" && h.IDValue != "" {
		id = fmt.Sprintf(" [%s=%s]", h.IDKey, h.IDValue)
	}
	return fmt.Sprintf("%s: Refresh complete%s", h.Resource.Addr, id)
}

func NewRefreshComplete(addr addrs.AbsResourceInstance, idKey, idValue string) Hook {
	return &refreshComplete{
		Resource: jsonentities.NewResourceAddr(addr),
		IDKey:    idKey,
		IDValue:  idValue,
	}
}

// EphemeralStart: triggered by PreOpen, PreRenew and PreClose hooks
type ephemeralStart struct {
	Resource jsonentities.ResourceAddr `json:"resource"`
	Msg      string
}

var _ Hook = (*ephemeralStart)(nil)

func (h *ephemeralStart) HookType() MessageType {
	return MessageEphemeralActionStart
}

func (h *ephemeralStart) String() string {
	return fmt.Sprintf("%s: %s", h.Resource.Addr, h.Msg)
}

func NewEphemeralStart(addr addrs.AbsResourceInstance, startMsg string) Hook {
	return &ephemeralStart{
		Resource: jsonentities.NewResourceAddr(addr),
		Msg:      startMsg,
	}
}

// EphemeralStop: triggered by PostOpen, PostRenew and PostClose hooks
type ephemeralStop struct {
	Resource jsonentities.ResourceAddr `json:"resource"`
	Msg      string
}

var _ Hook = (*ephemeralStop)(nil)

func (h *ephemeralStop) HookType() MessageType {
	return MessageEphemeralActionComplete
}

func (h *ephemeralStop) String() string {
	return fmt.Sprintf("%s: %s", h.Resource.Addr, h.Msg)
}

func NewEphemeralStop(addr addrs.AbsResourceInstance, startMsg string) Hook {
	return &ephemeralStop{
		Resource: jsonentities.NewResourceAddr(addr),
		Msg:      startMsg,
	}
}

// Convert the subset of plans.Action values we expect to receive into a
// present-tense verb for the applyStart hook message.
//
// NOTE: Open, Renew and Close missing on purpose since those have their own dedicated hooks.
func startActionVerb(action plans.Action) string {
	switch action {
	case plans.Create:
		return "Creating"
	case plans.Update:
		return "Modifying"
	case plans.Delete:
		return "Destroying"
	case plans.Read:
		return "Refreshing"
	case plans.CreateThenDelete, plans.DeleteThenCreate:
		// This is not currently possible to reach, as we receive separate
		// passes for create and delete
		return "Replacing"
	case plans.Forget:
		return "Removing"
	case plans.NoOp:
		// This should never be possible: a no-op planned change should not
		// be applied. We'll fall back to "Applying".
		fallthrough
	default:
		return "Applying"
	}
}

// Convert the subset of plans.Action values we expect to receive into a
// present-tense verb for the applyProgress hook message. This will be
// prefixed with "Still ", so it is lower-case.
//
// NOTE: Open, Renew and Close missing on purpose since those have their own dedicated hooks.
func progressActionVerb(action plans.Action) string {
	switch action {
	case plans.Create:
		return "creating"
	case plans.Update:
		return "modifying"
	case plans.Delete:
		return "destroying"
	case plans.Read:
		return "refreshing"
	case plans.CreateThenDelete, plans.DeleteThenCreate:
		// This is not currently possible to reach, as we receive separate
		// passes for create and delete
		return "replacing"
	case plans.Forget:
		return "removing"
	case plans.NoOp:
		// This should never be possible: a no-op planned change should not
		// be applied. We'll fall back to "applying".
		fallthrough
	default:
		return "applying"
	}
}

// Convert the subset of plans.Action values we expect to receive into a
// noun for the applyComplete and applyErrored hook messages. This will be
// combined into a phrase like "Creation complete after 1m4s".
//
// NOTE: Open, Renew and Close missing on purpose since those have their own dedicated hooks.
func actionNoun(action plans.Action) string {
	switch action {
	case plans.Create:
		return "Creation"
	case plans.Update:
		return "Modifications"
	case plans.Delete:
		return "Destruction"
	case plans.Read:
		return "Refresh"
	case plans.CreateThenDelete, plans.DeleteThenCreate:
		// This is not currently possible to reach, as we receive separate
		// passes for create and delete
		return "Replacement"
	case plans.Forget:
		return "Removal"
	case plans.NoOp:
		// This should never be possible: a no-op planned change should not
		// be applied. We'll fall back to "Apply".
		fallthrough
	default:
		return "Apply"
	}
}
