package claim

import (
	"fmt"
	"strconv"
	"time"

	"github.com/pkg/errors"
)

// Result tracks the result of an operation on a CNAB installation
type Result struct {
	// Id of the result.
	ID string `json:"id"`

	// ClaimId associated with the claim that generated the result.
	ClaimID string `json:"claimId"`

	// Claim associated with the Result.
	// This is not stored in the Result document but is loaded onto
	// the Result to build an in-memory hierarchy.
	// This may not always be set depending on how the Result was constructed.
	claim *Claim

	// Created timestamp of the result.
	Created time.Time `json:"created"`

	// Message communicates the outcome of the operation.
	Message string `json:"message,omitempty"`

	// Status of the operation, for example StatusSucceeded.
	Status string `json:"status"`

	// OutputMetadata generated by the operation, mapping from the output names to
	// metadata about the output.
	OutputMetadata OutputMetadata `json:"outputs,omitempty"`

	// Custom extension data applicable to a given runtime.
	Custom interface{} `json:"custom,omitempty"`
}

// NewResult creates a Result document with all required values set.
func NewResult(c Claim, status string) (Result, error) {
	id, err := NewULID()
	if err != nil {
		return Result{}, err
	}

	return Result{
		ID:             id,
		ClaimID:        c.ID,
		claim:          &c,
		Created:        time.Now(),
		Status:         status,
		OutputMetadata: OutputMetadata{},
	}, nil
}

// Validate the Result
func (r Result) Validate() error {
	if r.ID == "" {
		return errors.New("the result id must be set")
	}

	if r.ClaimID == "" {
		return errors.New("the claimID must be set")
	}

	switch r.Status {
	case StatusCanceled, StatusFailed, StatusPending, StatusRunning, StatusSucceeded, StatusUnknown:
		return nil
	}
	return fmt.Errorf("invalid status: %s", r.Status)
}

// HasLogs indicates if logs were persisted for the result.
func (r Result) HasLogs() bool {
	if r.OutputMetadata == nil {
		return false
	}
	_, ok := r.OutputMetadata[OutputInvocationImageLogs]
	return ok
}

// Results is a sortable list of Result documents.
type Results []Result

func (r Results) Len() int {
	return len(r)
}

func (r Results) Less(i, j int) bool {
	return r[i].ID < r[j].ID
}

func (r Results) Swap(i, j int) {
	r[i], r[j] = r[j], r[i]
}

// OutputMetadata is the output metadata from an operation.
// Any metadata can be stored, however this provides methods
// for safely querying and retrieving well-known metadata.
type OutputMetadata map[string]map[string]string

// GetMetadata for the specified output and key.
func (o OutputMetadata) GetMetadata(outputName string, metadataKey string) (string, bool) {
	if output, ok := o[outputName]; ok {
		if value, ok := output[metadataKey]; ok {
			return value, true
		}
	}

	return "", false
}

// SetMetadata for the specified output and key.
func (o *OutputMetadata) SetMetadata(outputName string, metadataKey string, value string) error {
	if *o == nil { // If the receiver is nil, initialize it to an empty map
		*o = OutputMetadata{}
	}
	metadata := *o

	outputMetadata, ok := metadata[outputName]
	if !ok {
		outputMetadata = map[string]string{
			metadataKey: value,
		}
		metadata[outputName] = outputMetadata
		return nil
	}

	outputMetadata[metadataKey] = value
	return nil
}

// GetGeneratedByBundle flag for the specified output.
func (o *OutputMetadata) GetGeneratedByBundle(outputName string) (bool, bool) {
	if generatedByBundleS, ok := o.GetMetadata(outputName, OutputGeneratedByBundle); ok {
		generatedByBundle, err := strconv.ParseBool(generatedByBundleS)
		return generatedByBundle, err == nil
	}

	return false, false
}

// SetGeneratedByBundle for the specified output.
func (o *OutputMetadata) SetGeneratedByBundle(outputName string, generatedByBundle bool) error {
	return o.SetMetadata(outputName, OutputGeneratedByBundle, strconv.FormatBool(generatedByBundle))
}

// GetContentDigest for the specified output.
func (o *OutputMetadata) GetContentDigest(outputName string) (string, bool) {
	return o.GetMetadata(outputName, OutputContentDigest)
}

// SetContentDigest for the specified output.
func (o *OutputMetadata) SetContentDigest(outputName string, contentDigest string) error {
	return o.SetMetadata(outputName, OutputContentDigest, contentDigest)
}
