/*
Copyright 2017 The Kubernetes Authors.

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 common

import (
	"encoding/json"
	"errors"
	"fmt"
	"strings"
	"sync"
	"time"

	"sigs.k8s.io/yaml"
)

const (
	// Busy state defines a resource being used.
	Busy = "busy"
	// Cleaning state defines a resource being cleaned
	Cleaning = "cleaning"
	// Dirty state defines a resource that needs cleaning
	Dirty = "dirty"
	// Free state defines a resource that is usable
	Free = "free"
	// Leased state defines a resource being leased in order to make a new resource
	Leased = "leased"
	// ToBeDeleted is used for resources about to be deleted, they will be verified by a cleaner which mark them as tombstone
	ToBeDeleted = "toBeDeleted"
	// Tombstone is the state in which a resource can safely be deleted
	Tombstone = "tombstone"
	// Other is used to agglomerate unspecified states for metrics reporting
	Other = "other"
)

var (
	// KnownStates is the set of all known states, excluding "other".
	KnownStates = []string{
		Busy,
		Cleaning,
		Dirty,
		Free,
		Leased,
		ToBeDeleted,
		Tombstone,
	}
)

// UserData is a map of Name to user defined interface, serialized into a string
type UserData struct {
	sync.Map
}

// UserDataMap is the standard Map version of UserMap, it is used to ease UserMap creation.
type UserDataMap map[string]string

// LeasedResources is a list of resources name that used in order to create another resource by Mason
type LeasedResources []string

// Duration is a wrapper around time.Duration that parses times in either
// 'integer number of nanoseconds' or 'duration string' formats and serializes
// to 'duration string' format.
type Duration struct {
	*time.Duration
}

// UnmarshalJSON implement the JSON Unmarshaler interface in order to be able parse string to time.Duration.
func (d *Duration) UnmarshalJSON(b []byte) error {
	if err := json.Unmarshal(b, &d.Duration); err == nil {
		// b was an integer number of nanoseconds.
		return nil
	}
	// b was not an integer. Assume that it is a duration string.

	var str string
	err := json.Unmarshal(b, &str)
	if err != nil {
		return err
	}

	pd, err := time.ParseDuration(str)
	if err != nil {
		return err
	}
	d.Duration = &pd
	return nil
}

// Resource abstracts any resource type that can be tracked by boskos
type Resource struct {
	Type       string    `json:"type"`
	Name       string    `json:"name"`
	State      string    `json:"state"`
	Owner      string    `json:"owner"`
	LastUpdate time.Time `json:"lastupdate"`
	// Customized UserData
	UserData *UserData `json:"userdata"`
	// Used to clean up dynamic resources
	ExpirationDate *time.Time `json:"expiration-date,omitempty"`
}

// ResourceEntry is resource config format defined from config.yaml
type ResourceEntry struct {
	Type     string        `json:"type"`
	State    string        `json:"state"`
	Names    []string      `json:"names"`
	MaxCount int           `json:"max-count,omitempty"`
	MinCount int           `json:"min-count,omitempty"`
	LifeSpan *Duration     `json:"lifespan,omitempty"`
	Config   ConfigType    `json:"config,omitempty"`
	Needs    ResourceNeeds `json:"needs,omitempty"`
}

func (re *ResourceEntry) IsDRLC() bool {
	return len(re.Names) == 0
}

// BoskosConfig defines config used by boskos server
type BoskosConfig struct {
	Resources []ResourceEntry `json:"resources"`
}

// Metric contains analytics about a specific resource type
type Metric struct {
	Type    string         `json:"type"`
	Current map[string]int `json:"current"`
	Owners  map[string]int `json:"owner"`
	// TODO: implements state transition metrics
}

// NewMetric returns a new Metric struct.
func NewMetric(rtype string) Metric {
	return Metric{
		Type:    rtype,
		Current: map[string]int{},
		Owners:  map[string]int{},
	}
}

// NewResource creates a new Boskos Resource.
func NewResource(name, rtype, state, owner string, t time.Time) Resource {
	// If no state defined, mark as Free
	if state == "" {
		state = Free
	}
	return Resource{
		Name:       name,
		Type:       rtype,
		State:      state,
		Owner:      owner,
		LastUpdate: t,
	}
}

// NewResourcesFromConfig parse the a ResourceEntry into a list of resources
func NewResourcesFromConfig(e ResourceEntry) []Resource {
	var resources []Resource
	for _, name := range e.Names {
		resources = append(resources, NewResource(name, e.Type, e.State, "", time.Time{}))
	}
	return resources
}

// UserDataFromMap returns a UserData from a map
func UserDataFromMap(m UserDataMap) *UserData {
	ud := &UserData{}
	for k, v := range m {
		ud.Store(k, v)
	}
	return ud
}

// UserDataNotFound will be returned if requested resource does not exist.
type UserDataNotFound struct {
	ID string
}

func (ud *UserDataNotFound) Error() string {
	return fmt.Sprintf("user data ID %s does not exist", ud.ID)
}

// ResourceByName helps sorting resources by name
type ResourceByName []Resource

func (ut ResourceByName) Len() int           { return len(ut) }
func (ut ResourceByName) Swap(i, j int)      { ut[i], ut[j] = ut[j], ut[i] }
func (ut ResourceByName) Less(i, j int) bool { return ut[i].Name < ut[j].Name }

// CommaSeparatedStrings is used to parse comma separated string flag into a list of strings
type CommaSeparatedStrings []string

func (r *CommaSeparatedStrings) String() string {
	return fmt.Sprint(*r)
}

// Set parses the flag value into a CommaSeparatedStrings
func (r *CommaSeparatedStrings) Set(value string) error {
	if len(*r) > 0 {
		return errors.New("resTypes flag already set")
	}
	for _, rtype := range strings.Split(value, ",") {
		*r = append(*r, rtype)
	}
	return nil
}

func (r *CommaSeparatedStrings) Type() string {
	return "commaSeparatedStrings"
}

// UnmarshalJSON implements JSON Unmarshaler interface
func (ud *UserData) UnmarshalJSON(data []byte) error {
	tmpMap := UserDataMap{}
	if err := json.Unmarshal(data, &tmpMap); err != nil {
		return err
	}
	ud.FromMap(tmpMap)
	return nil
}

// MarshalJSON implements JSON Marshaler interface
func (ud *UserData) MarshalJSON() ([]byte, error) {
	return json.Marshal(ud.ToMap())
}

// Extract unmarshalls a string a given struct if it exists
func (ud *UserData) Extract(id string, out interface{}) error {
	content, ok := ud.Load(id)
	if !ok {
		return &UserDataNotFound{id}
	}
	return yaml.Unmarshal([]byte(content.(string)), out)
}

// User Data are used to store custom information mainly by Mason and Masonable implementation.
// Mason used a LeasedResource keys to store information about other resources that used to
// create the given resource.

// Set marshalls a struct to a string into the UserData
func (ud *UserData) Set(id string, in interface{}) error {
	b, err := yaml.Marshal(in)
	if err != nil {
		return err
	}
	ud.Store(id, string(b))
	return nil
}

// Update updates existing UserData with new UserData.
// If a key as an empty string, the key will be deleted
func (ud *UserData) Update(new *UserData) *UserData {
	if new == nil {
		return ud
	}
	new.Range(func(key, value interface{}) bool {
		if value.(string) != "" {
			ud.Store(key, value)
		} else {
			ud.Delete(key)
		}
		return true
	})
	return ud
}

// ToMap converts a UserData to UserDataMap
func (ud *UserData) ToMap() UserDataMap {
	if ud == nil {
		return nil
	}
	m := UserDataMap{}
	ud.Range(func(key, value interface{}) bool {
		m[key.(string)] = value.(string)
		return true
	})
	return m
}

// FromMap feels updates user data from a map
func (ud *UserData) FromMap(m UserDataMap) {
	for key, value := range m {
		ud.Store(key, value)
	}
}

func ResourceTypeNotFoundMessage(rType string) string {
	return fmt.Sprintf("resource type %q does not exist", rType)
}
