package agent

import (
	"fmt"
	"net/http"
	"strings"

	log "github.com/Sirupsen/logrus"
	"github.com/aws/aws-sdk-go/aws/awserr"
	"github.com/aws/aws-sdk-go/aws/credentials"
)

// MissingSecretError is returned on missing secret
type MissingSecretError struct {
	secret string
}

func (m MissingSecretError) Error() string {
	return fmt.Sprintf("no such secret: %s", m.secret)
}

// InvalidStateFile error type
type InvalidStateFile struct {
	message string
}

func (e InvalidStateFile) Error() string {
	return e.message
}

// TemplatingError tracks errors associated during the template output process
type TemplatingError struct {
	errors map[*Template]error
}

// NewTemplatingError creates a new output error with a template map
func NewTemplatingError() *TemplatingError {
	return &TemplatingError{errors: map[*Template]error{}}
}

// Add provides a convenience function for adding output errors
func (t *TemplatingError) Add(template *Template, err error) {
	t.errors[template] = err
	template.Error = err.Error()
}

// Count allows you to determine whether or not there are actually any errors
func (t *TemplatingError) Count() int {
	return len(t.errors)
}

func (t *TemplatingError) Error() string {
	err := "Template Output Errors: "

	for template, tError := range t.errors {
		err = strings.Join([]string{err, fmt.Sprintf(
			"Template Error:\nSource: %s\nDestination: %s Command: %s\nError: %s",
			template.Source,
			template.Destination,
			template.Command,
			tError.Error(),
		)}, "\n")
	}

	return err
}

// SecretFetchError is returned on error fetching a secret from manager
type SecretFetchError struct {
	Secret string
	Err    error
}

func (e SecretFetchError) Error() string {
	return fmt.Sprintf("error retrieving secret '%s' from manager: %s", e.Secret, e.Err.Error())
}

// NoConfiguredTemplatesError is returned when no templates are found in config
type NoConfiguredTemplatesError struct{}

const noConfiguredTemplatesErrorMessage = "no templates configured"

func (e NoConfiguredTemplatesError) Error() string {
	return noConfiguredTemplatesErrorMessage
}

// RecentError implements error interface as well as determines if the error is
// a system error (i.e. aws service being unavailable) or not.
type RecentError interface {
	Error() string
	SystemError() bool
}

type recentError struct {
	error
	systemError bool
}

// SystemError describes if the error is caused by a system issue (i.e. aws
// service being unavailable)
func (r *recentError) SystemError() bool { return r.systemError }

func isSystemError(err error) bool {
	switch t := err.(type) {
	case MissingSecretError, *TemplatingError, InvalidStateFile, NoConfiguredTemplatesError, *IncorrectAWSAccount:
		return false
	case awserr.Error:
		if t == credentials.ErrNoValidProvidersFoundInChain {
			return false
		}

		// try casting as RequestFailure and check if it is 403
		if requestFailure, ok := err.(awserr.RequestFailure); ok {
			if requestFailure.StatusCode() == http.StatusForbidden {
				return false
			}
		}

		if strings.HasPrefix(t.Message(), "AccessDeniedException") {
			return false
		}
		return true
	default:
		return true
	}
}

// SetRecentError writes recent error to state
func (s *State) SetRecentError(err error) {
	if err == nil {
		s.RecentError = nil
		return
	}

	s.RecentError = &recentError{
		error:       err,
		systemError: isSystemError(err),
	}
}

// Sets recent errors and logs according to error type
func (s *State) handleError(err error) {
	s.SetRecentError(err)
	if s.RecentError.SystemError() {
		log.Errorf("[System Error] Agent Watch Errors: %s\nContinuing...", err.Error())

	} else {
		log.Errorf("Agent Watch Errors: %s\nContinuing...", err.Error())

	}
}

// IncorrectAWSAccount is returned when Account ID does not match sandstormAccountNumber
type IncorrectAWSAccount struct {
	accountNumber string
}

func (i IncorrectAWSAccount) Error() string {
	return fmt.Sprintf("Unable to access Sandstorm account, please verify roleArn in Sandstorm config file. Account used was %s, should be %s", i.accountNumber, sandstormAccountNumber)
}
