// Copyright © 2016 Asteris, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package resource

import (
	"errors"
	"fmt"
)

// StatusLevel will be used as a level in Status. It indicates if a resource
// needs to be changed, as well as fatal conditions.
type StatusLevel uint32

const (
	// StatusNoChange means no changes are necessary. This status signals that
	// execution of dependent resources can continue.
	StatusNoChange StatusLevel = iota

	// StatusWontChange indicates an acceptable delta that wont be corrected.
	// This status signals that execution of dependent resources can continue.
	StatusWontChange

	// StatusWillChange indicates an unacceptable delta that will be corrected.
	// This status signals that execution of dependent resources can continue.
	StatusWillChange

	// StatusMayChange indicates an unacceptable delta that may be corrected. This
	// is considered a warning state that indicates that the resource needs to
	// change but is likely dependent on the succesful execution of another
	// resource. This status signals that execution of dependent resources can
	// continue.
	StatusMayChange

	// StatusCantChange indicates an unacceptable delta that can't be corrected.
	// This is just like StatusFatal except the user will see that the resource
	// needs to change, but can't because of the condition specified in your
	// messaging. This status halts execution of dependent resources.
	StatusCantChange

	// StatusFatal indicates an error. This is just like StatusCantChange except
	// it does not imply that there are changes to be made. This status halts
	// execution of dependent resources.
	StatusFatal
)

// error messages
var (
	// ErrStatusCantChange is returned when an error is not set but the level is
	// StatusCantChange
	ErrStatusCantChange = errors.New("resource cannot change because of an error")

	// ErrStatusFatal is returned when an error is not set but the level is
	// StatusFatal
	ErrStatusFatal = errors.New("resource encountered an error")
)

func (l StatusLevel) String() string {
	switch l {
	case StatusNoChange:
		return "no change"

	case StatusWontChange:
		return "won't change"

	case StatusWillChange:
		return "will change"

	case StatusMayChange:
		return "may change"

	case StatusCantChange:
		return "can't change"

	case StatusFatal:
		return "fatal"
	}

	return "invalid status level"
}

type badDep struct {
	ID     string
	Status TaskStatus
}

// TaskStatus represents the results of Check called during planning or
// application.
type TaskStatus interface {
	Diffs() map[string]Diff
	StatusCode() StatusLevel
	Messages() []string
	HasChanges() bool
	Error() error
	Warning() string
	UpdateExportedFields(Task) error
	ExportedFields() FieldMap
}

// Status is the default TaskStatus implementation
type Status struct {
	// Differences contains the things that will change as a part of this
	// Status. This will be used almost exclusively in the Check phase of
	// operations on resources. Use `NewStatus` to get a Status with this
	// initialized properly.
	Differences map[string]Diff

	// Output is the human-consumable fields on this struct. Output will be
	// returned as the Status' messages
	Output []string

	// Level indicates the change level of the status. Level is a gradation (see
	// the Status* contsts above.)
	Level StatusLevel

	// Exported fields contains the fields that should be exported through lookup
	exportedFields FieldMap

	error       error
	warning     string
	failingDeps []badDep
}

// UpdateExportedFields sets the exported fields in the status
func (t *Status) UpdateExportedFields(input Task) error {
	fields, err := LookupMapFromInterface(input)
	if err != nil {
		return err
	}
	t.exportedFields = fields
	return nil
}

// ExportedFields returns the exported fields from the status
func (t *Status) ExportedFields() FieldMap {
	if t.exportedFields == nil {
		t.exportedFields = make(FieldMap)
	}
	return t.exportedFields
}

// NewStatus returns a Status with all fields initialized
func NewStatus() *Status {
	return &Status{
		Differences: map[string]Diff{},
	}
}

// SetWarning sets a warning on a status
func (t *Status) SetWarning(warning string) {
	if t == nil {
		*t = *NewStatus()
	}
	t.warning = warning
}

// SetError sets an error on a status
func (t *Status) SetError(err error) {
	if t == nil {
		*t = *NewStatus()
	}

	switch t.Level {
	case StatusWillChange, StatusCantChange:
		t.Level = StatusCantChange

	default:
		t.Level = StatusFatal
	}

	t.error = err
}

// Warning returns the warning message, if set.
func (t *Status) Warning() string {
	return t.warning
}

// Error returns an error, if set. If the level is StatusCantChange or
// StatusFatal and an error is not set, Error will generate an appropriate
// error message.
func (t *Status) Error() error {
	if t.error == nil {
		switch t.Level {
		case StatusCantChange:
			return ErrStatusCantChange

		case StatusFatal:
			return ErrStatusFatal
		}
	}

	return t.error
}

// Diffs returns the internal differences
func (t *Status) Diffs() map[string]Diff {
	return t.Differences
}

// StatusCode returns the current warning level
func (t *Status) StatusCode() StatusLevel {
	return t.Level
}

// Messages returns the current output slice
func (t *Status) Messages() []string {
	return t.Output
}

// HasChanges returns the WillChange value
func (t *Status) HasChanges() bool {
	if t.Level == StatusFatal {
		return false
	}

	if t.Level == StatusWillChange || t.Level == StatusCantChange || t.Level == StatusMayChange {
		return true
	}

	for _, diff := range t.Diffs() {
		if diff.Changes() {
			return true
		}
	}

	return false
}

// HealthCheck provides a default health check implementation for statuses
func (t *Status) HealthCheck() (status *HealthStatus, err error) {
	status = &HealthStatus{TaskStatus: t, FailingDeps: make(map[string]string)}
	if !t.HasChanges() && len(t.failingDeps) == 0 {
		return
	}

	// There are changes or failing dependencies so the health check is at least
	// at a warning status.
	status.UpgradeWarning(StatusWarning)

	for _, failingDep := range t.failingDeps {
		status.FailingDeps[failingDep.ID] = fmt.Sprintf("returned %d", failingDep.Status.StatusCode())
	}
	if t.StatusCode() >= 2 {
		status.UpgradeWarning(StatusError)
	}
	return
}

// FailingDep tracks a new failing dependency
func (t *Status) FailingDep(id string, stat TaskStatus) {
	t.failingDeps = append(t.failingDeps, badDep{ID: id, Status: stat})
}

// AddDifference adds a TextDiff to the Differences map
func (t *Status) AddDifference(name, original, current, defaultVal string) {
	t.Differences = AddTextDiff(t.Differences, name, original, current, defaultVal)
}

// AddMessage adds a human-readable message(s) to the output
func (t *Status) AddMessage(message ...string) {
	t.Output = append(t.Output, message...)
}

// RaiseLevel raises the status level to the given level
func (t *Status) RaiseLevel(level StatusLevel) {
	if level > t.Level {
		t.Level = level
	}
}

// RaiseLevelForDiffs raises the status level to StatusWillChange if there are
// differences in the Differences map
func (t *Status) RaiseLevelForDiffs() {
	if AnyChanges(t.Differences) {
		t.RaiseLevel(StatusWillChange)
	}
}

// Diff represents a difference
type Diff interface {
	Original() string
	Current() string
	Changes() bool
}

// TextDiff is the default Diff implementation
type TextDiff struct {
	Default string
	Values  [2]string
}

// Original returns the unmodified value of the diff
func (t TextDiff) Original() string {
	if t.Values[0] == "" {
		return t.Default
	}
	return t.Values[0]
}

// Current returns the modified value of the diff
func (t TextDiff) Current() string {
	if t.Values[1] == "" {
		return t.Default
	}
	return t.Values[1]
}

// Changes is true if the Original and Current values differ
func (t TextDiff) Changes() bool {
	return t.Values[0] != t.Values[1]
}

// AnyChanges takes a diff map and returns true if any of the diffs in the map
// have changes.
func AnyChanges(diffs map[string]Diff) bool {
	for _, diffIf := range diffs {
		diff, ok := diffIf.(Diff)
		if !ok {
			panic("invalid conversion")
		}
		if diff.Changes() {
			return true
		}
	}
	return false
}

// AddTextDiff inserts a new TextDiff into a map of names to Diffs
func AddTextDiff(m map[string]Diff, name, original, current, defaultVal string) map[string]Diff {
	if m == nil {
		m = make(map[string]Diff)
	}
	m[name] = TextDiff{Values: [2]string{original, current}, Default: defaultVal}
	return m
}
