// 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 shell

import (
	"fmt"
	"strings"

	"github.com/asteris-llc/converge/resource"
	"golang.org/x/net/context"
)

// Shell is a structure representing a task.
type Shell struct {
	CmdGenerator CommandExecutor

	// the check statement
	CheckStmt string `export:"check"`

	// the apply statement
	ApplyStmt string `export:"apply"`

	// the working directory of the task
	Dir string `export:"dir"`

	// environment variables configured for the task
	Env []string `export:"env"`

	// the status of the task
	Status *CommandResults `re-export-as:"status"`

	// the status of the check phase
	CheckStatus *CommandResults `export:"checkstatus"`

	// the status of the health check
	HealthStatus *resource.HealthStatus `export:"healthstatus"`

	renderer       resource.Renderer
	ctx            context.Context
	exportedFields resource.FieldMap
}

// Check passes through to shell.Shell.Check() and then sets the health status
func (s *Shell) Check(ctx context.Context, r resource.Renderer) (resource.TaskStatus, error) {
	s.renderer = r
	results, err := s.CmdGenerator.Run(s.CheckStmt)
	if err != nil {
		return nil, err
	}
	if s.Status == nil {
		s.Status = s.Status.Cons("check", results)
	}
	if s.CheckStatus == nil {
		s.CheckStatus = results
	}
	return s, nil
}

// ExportedFields returns the exported field map
func (s *Shell) ExportedFields() resource.FieldMap {
	if s.exportedFields == nil {
		s.exportedFields = make(resource.FieldMap)
	}
	return s.exportedFields
}

// UpdateExportedFields is a nop
func (s *Shell) UpdateExportedFields(resource.Task) error {
	fields, err := resource.LookupMapFromStruct(s)
	if err != nil {
		return err
	}
	s.exportedFields = fields
	return nil
}

// Apply is a NOP for health checks
func (s *Shell) Apply(context.Context) (resource.TaskStatus, error) {
	if cg, ok := s.CmdGenerator.(*CommandGenerator); ok {
		s.CmdGenerator = cg
	}
	results, err := s.CmdGenerator.Run(s.ApplyStmt)
	if err == nil {
		s.Status = s.Status.Cons("apply", results)
	}
	return s, err
}

// resource.TaskStatus functions

// Value provides a value for the shell, which is the stdout data from the last
// executed command.
func (s *Shell) Value() string {
	return s.Status.Stdout
}

// Diffs is required to implement resource.TaskStatus but there is no mechanism
// for defining diffs for shell operations, so returns a nil map.
func (s *Shell) Diffs() map[string]resource.Diff {
	return nil
}

// StatusCode returns the status code of the most recently executed command
func (s *Shell) StatusCode() resource.StatusLevel {
	if s.Status == nil {
		return resource.StatusFatal
	}

	if s.Status.ExitStatus == 0 {
		return resource.StatusNoChange
	}

	return resource.StatusWillChange
}

// Messages returns a summary of the first execution of check and/or apply.
// Subsequent runs are surpressed.
func (s *Shell) Messages() (messages []string) {
	if s.Status == nil {
		return
	}

	if s.Dir != "" {
		messages = append(messages, fmt.Sprintf("dir (%s)", s.Dir))
	}

	if len(s.Env) > 0 {
		messages = append(messages, fmt.Sprintf("env (%s)", strings.Join(s.Env, " ")))
	}

	messages = append(messages, s.Status.Reverse().UniqOp().SummarizeAll()...)
	return
}

// HasChanges returns true if changes are required as determined by the the most
// recent run of check.
func (s *Shell) HasChanges() bool {
	if s.Status == nil {
		return false
	}
	return (s.Status.ExitStatus != 0)
}

// healthcheck.Check functions

// FailingDep tracks a failing dependency
func (s *Shell) FailingDep(name string, task resource.TaskStatus) {
	if s.HealthStatus == nil {
		s.HealthStatus = new(resource.HealthStatus)
		s.HealthStatus.FailingDeps = make(map[string]string)
	}
	s.HealthStatus.FailingDeps[name] = name
}

// HealthCheck performs a health check
func (s *Shell) HealthCheck() (*resource.HealthStatus, error) {
	var err error
	if s.HealthStatus == nil {
		err = s.updateHealthStatus()
	}
	return s.HealthStatus, err
}

// Error is required for TaskStatus
func (s *Shell) Error() error {
	if s.HealthStatus != nil {
		return s.HealthStatus.Error()
	}

	return nil
}

// Warning is required for TaskStatus
func (s *Shell) Warning() string {
	return ""
}

func (s *Shell) updateHealthStatus() error {
	if s.Status == nil {
		fmt.Println("[INFO] health status requested with no plan, running check")
		if _, err := s.Check(s.ctx, s.renderer); err != nil {
			return err
		}
	}
	if s.HealthStatus == nil {
		s.HealthStatus = new(resource.HealthStatus)
	}
	s.HealthStatus.TaskStatus = s
	s.HealthStatus.WarningLevel = exitStatusToWarningLevel(s.Status.ExitStatus)
	return nil
}

func exitStatusToWarningLevel(status uint32) resource.HealthStatusCode {
	if status == 0 {
		return resource.StatusHealthy
	} else if status == 1 {
		return resource.StatusWarning
	}
	return resource.StatusError
}
