/*
Real-time Online/Offline Charging System (OCS) for Telecom & ISP environments
Copyright (C) ITsysCOM GmbH

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>
*/

package utils

import (
	"time"
)

// ResourceProfile represents the user configuration for the resource
type ResourceProfile struct {
	Tenant            string
	ID                string // identifier of this resource
	FilterIDs         []string
	UsageTTL          time.Duration // auto-expire the usage after this duration
	Limit             float64       // limixt value
	AllocationMessage string        // message returned by the winning resource on allocation
	Blocker           bool          // blocker flag to stop processing on filters matched
	Stored            bool
	Weights           DynamicWeights // Weight to sort the resources
	ThresholdIDs      []string       // Thresholds to check after changing Limit
}

// Clone clones *ResourceProfile (lkID excluded)
func (rp *ResourceProfile) Clone() *ResourceProfile {
	if rp == nil {
		return nil
	}
	clone := &ResourceProfile{

		Tenant:            rp.Tenant,
		ID:                rp.ID,
		UsageTTL:          rp.UsageTTL,
		Limit:             rp.Limit,
		AllocationMessage: rp.AllocationMessage,
		Blocker:           rp.Blocker,
		Stored:            rp.Stored,
	}
	if rp.FilterIDs != nil {
		clone.FilterIDs = make([]string, len(rp.FilterIDs))
		copy(clone.FilterIDs, rp.FilterIDs)
	}
	if rp.ThresholdIDs != nil {
		clone.ThresholdIDs = make([]string, len(rp.ThresholdIDs))
		copy(clone.ThresholdIDs, rp.ThresholdIDs)
	}
	if rp.Weights != nil {
		clone.Weights = rp.Weights.Clone()
	}
	return clone
}

// CacheClone returns a clone of ResourceProfile used by ltcache CacheCloner
func (rp *ResourceProfile) CacheClone() any {
	return rp.Clone()
}

// ResourceProfileWithAPIOpts is used in replicatorV1 for dispatcher
type ResourceProfileWithAPIOpts struct {
	*ResourceProfile
	APIOpts map[string]any
}

// TenantID returns unique identifier of the ResourceProfile in a multi-tenant environment
func (rp *ResourceProfile) TenantID() string {
	return ConcatenatedKey(rp.Tenant, rp.ID)
}

// ResourceUsage represents an usage counted
type ResourceUsage struct {
	Tenant     string
	ID         string // Unique identifier of this ResourceUsage, Eg: FreeSWITCH UUID
	ExpiryTime time.Time
	Units      float64 // Number of units used
}

// TenantID returns the concatenated key between tenant and ID
func (ru *ResourceUsage) TenantID() string {
	return ConcatenatedKey(ru.Tenant, ru.ID)
}

// isActive checks ExpiryTime at some time
func (ru *ResourceUsage) IsActive(atTime time.Time) bool {
	return ru.ExpiryTime.IsZero() || ru.ExpiryTime.Sub(atTime) > 0
}

// Clone duplicates ru
func (ru *ResourceUsage) Clone() (cln *ResourceUsage) {
	cln = new(ResourceUsage)
	*cln = *ru
	return
}

// Resource represents a resource in the system
// not thread safe, needs locking at process level
type Resource struct {
	Tenant string
	ID     string
	Usages map[string]*ResourceUsage
	TTLIdx []string // holds ordered list of ResourceIDs based on their TTL, empty if feature is disabled
}

// Clone clones *Resource (lkID excluded)
func (r *Resource) Clone() *Resource {
	if r == nil {
		return nil
	}
	clone := &Resource{
		Tenant: r.Tenant,
		ID:     r.ID,
	}
	if r.Usages != nil {
		clone.Usages = make(map[string]*ResourceUsage, len(r.Usages))
		for key, usage := range r.Usages {
			clone.Usages[key] = usage.Clone()
		}
	}
	if r.TTLIdx != nil {
		clone.TTLIdx = make([]string, len(r.TTLIdx))
		copy(clone.TTLIdx, r.TTLIdx)
	}
	return clone
}

// CacheClone returns a clone of Resource used by ltcache CacheCloner
func (r *Resource) CacheClone() any {
	return r.Clone()
}

// ResourceWithAPIOpts is used in replicatorV1 for dispatcher
type ResourceWithAPIOpts struct {
	*Resource
	APIOpts map[string]any
}

// TenantID returns the unique ID in a multi-tenant environment
func (r *Resource) TenantID() string {
	return ConcatenatedKey(r.Tenant, r.ID)
}

// TotalUsage returns the sum of all usage units
// Exported to be used in FilterS
func (r *Resource) TotalUsage() float64 {
	var tu float64
	for _, ru := range r.Usages {
		tu += ru.Units
	}
	return tu
}

// AsMapStringInterface converts Resource struct to map[string]any
func (rp *Resource) AsMapStringInterface() map[string]any {
	if rp == nil {
		return nil
	}
	return map[string]any{
		Tenant: rp.Tenant,
		ID:     rp.ID,
		Usages: rp.Usages,
		TTLIdx: rp.TTLIdx,
	}
}

// MapStringInterfaceToResource converts map[string]any to Resource struct
func MapStringInterfaceToResource(m map[string]any) *Resource {
	rp := &Resource{}
	if v, ok := m[Tenant].(string); ok {
		rp.Tenant = v
	}
	if v, ok := m[ID].(string); ok {
		rp.ID = v
	}
	rp.Usages = InterfaceToMapStringResourceUsage(m[Usages])
	rp.TTLIdx = InterfaceToStringSlice(m[TTLIdx])
	return rp
}

// InterfaceToMapStringResourceUsage converts any to map[string]*ResourceUsage
func InterfaceToMapStringResourceUsage(v any) map[string]*ResourceUsage {
	if v == nil {
		return nil
	}
	switch val := v.(type) {
	case map[string]*ResourceUsage:
		return val
	case map[string]any:
		result := make(map[string]*ResourceUsage)
		for k, v := range val {
			if balMap, ok := v.(map[string]any); ok {
				result[k] = MapStringInterfaceToResourceUsage(balMap)
			} else if bal, ok := v.(*ResourceUsage); ok {
				result[k] = bal
			}
		}
		return result
	}
	return nil
}

// MapStringInterfaceToResourceUsage converts map[string]any to *ResourceUsage
func MapStringInterfaceToResourceUsage(m map[string]any) *ResourceUsage {
	resUsage := &ResourceUsage{}
	if v, ok := m[Tenant].(string); ok {
		resUsage.Tenant = v
	}
	if v, ok := m[ID].(string); ok {
		resUsage.ID = v
	}
	if v, ok := m[ExpiryTime].(string); ok {
		if t, err := time.Parse(time.RFC3339, v); err == nil {
			resUsage.ExpiryTime = t
		}
	}
	if v, ok := m[Units].(float64); ok {
		resUsage.Units = v
	}
	return resUsage
}

// Available returns the available number of units
// Exported method to be used by filterS
func (r *ResourceWithConfig) Available() float64 {
	return r.Config.Limit - r.TotalUsage()
}

type ResourceWithConfig struct {
	*Resource
	Config *ResourceProfile
}

func (rp *ResourceProfile) Set(path []string, val any, _ bool) (err error) {
	if len(path) != 1 {
		return ErrWrongPath
	}
	switch path[0] {
	default:
		return ErrWrongPath
	case Tenant:
		rp.Tenant = IfaceAsString(val)
	case ID:
		rp.ID = IfaceAsString(val)
	case FilterIDs:
		var valA []string
		valA, err = IfaceAsStringSlice(val)
		rp.FilterIDs = append(rp.FilterIDs, valA...)
	case UsageTTL:
		rp.UsageTTL, err = IfaceAsDuration(val)
	case Limit:
		if val != EmptyString {
			rp.Limit, err = IfaceAsFloat64(val)
		}
	case AllocationMessage:
		rp.AllocationMessage = IfaceAsString(val)
	case Blocker:
		rp.Blocker, err = IfaceAsBool(val)
	case Stored:
		rp.Stored, err = IfaceAsBool(val)
	case Weights:
		if val != EmptyString {
			rp.Weights, err = NewDynamicWeightsFromString(IfaceAsString(val), InfieldSep, ANDSep)
		}
	case ThresholdIDs:
		var valA []string
		valA, err = IfaceAsStringSlice(val)
		rp.ThresholdIDs = append(rp.ThresholdIDs, valA...)
	}
	return
}

func (rp *ResourceProfile) Merge(v2 any) {
	vi := v2.(*ResourceProfile)
	if len(vi.Tenant) != 0 {
		rp.Tenant = vi.Tenant
	}
	if len(vi.ID) != 0 {
		rp.ID = vi.ID
	}
	rp.FilterIDs = append(rp.FilterIDs, vi.FilterIDs...)
	rp.ThresholdIDs = append(rp.ThresholdIDs, vi.ThresholdIDs...)
	if len(vi.AllocationMessage) != 0 {
		rp.AllocationMessage = vi.AllocationMessage
	}
	if vi.UsageTTL != 0 {
		rp.UsageTTL = vi.UsageTTL
	}
	if vi.Limit != 0 {
		rp.Limit = vi.Limit
	}
	if vi.Blocker {
		rp.Blocker = vi.Blocker
	}
	if vi.Stored {
		rp.Stored = vi.Stored
	}
	rp.Weights = append(rp.Weights, vi.Weights...)
}

func (rp *ResourceProfile) String() string { return ToJSON(rp) }
func (rp *ResourceProfile) FieldAsString(fldPath []string) (_ string, err error) {
	var val any
	if val, err = rp.FieldAsInterface(fldPath); err != nil {
		return
	}
	return IfaceAsString(val), nil
}
func (rp *ResourceProfile) FieldAsInterface(fldPath []string) (_ any, err error) {
	if len(fldPath) != 1 {
		return nil, ErrNotFound
	}
	switch fldPath[0] {
	default:
		fld, idx := GetPathIndex(fldPath[0])
		if idx != nil {
			switch fld {
			case ThresholdIDs:
				if *idx < len(rp.ThresholdIDs) {
					return rp.ThresholdIDs[*idx], nil
				}
			case FilterIDs:
				if *idx < len(rp.FilterIDs) {
					return rp.FilterIDs[*idx], nil
				}
			}
		}
		return nil, ErrNotFound
	case Tenant:
		return rp.Tenant, nil
	case ID:
		return rp.ID, nil
	case FilterIDs:
		return rp.FilterIDs, nil
	case UsageTTL:
		return rp.UsageTTL, nil
	case Limit:
		return rp.Limit, nil
	case AllocationMessage:
		return rp.AllocationMessage, nil
	case Blocker:
		return rp.Blocker, nil
	case Stored:
		return rp.Stored, nil
	case Weights:
		return rp.Weights, nil
	case ThresholdIDs:
		return rp.ThresholdIDs, nil
	}
}

// ResourceProfileLockKey returns the ID used to lock a resourceProfile with guardian
func ResourceProfileLockKey(tnt, id string) string {
	return ConcatenatedKey(CacheResourceProfiles, tnt, id)
}

// ResourceLockKey returns the ID used to lock a resource with guardian
func ResourceLockKey(tnt, id string) string {
	return ConcatenatedKey(CacheResources, tnt, id)
}

// AsMapStringInterface converts ResourceProfile struct to map[string]any
func (rp *ResourceProfile) AsMapStringInterface() map[string]any {
	if rp == nil {
		return nil
	}
	return map[string]any{
		Tenant:            rp.Tenant,
		ID:                rp.ID,
		FilterIDs:         rp.FilterIDs,
		UsageTTL:          rp.UsageTTL,
		Limit:             rp.Limit,
		AllocationMessage: rp.AllocationMessage,
		Blocker:           rp.Blocker,
		Stored:            rp.Stored,
		Weights:           rp.Weights,
		ThresholdIDs:      rp.ThresholdIDs,
	}
}

// MapStringInterfaceToResourceProfile converts map[string]any to ResourceProfile struct
func MapStringInterfaceToResourceProfile(m map[string]any) (rp *ResourceProfile, err error) {
	rp = &ResourceProfile{}
	if v, ok := m[Tenant].(string); ok {
		rp.Tenant = v
	}
	if v, ok := m[ID].(string); ok {
		rp.ID = v
	}
	rp.FilterIDs = InterfaceToStringSlice(m[FilterIDs])
	if v, ok := m[UsageTTL].(string); ok {
		if dur, err := time.ParseDuration(v); err != nil {
			return nil, err
		} else {
			rp.UsageTTL = dur
		}
	} else if v, ok := m[UsageTTL].(float64); ok { // for -1 cases
		rp.UsageTTL = time.Duration(v)
	}
	if v, ok := m[Limit].(float64); ok {
		rp.Limit = v
	}
	if v, ok := m[AllocationMessage].(string); ok {
		rp.AllocationMessage = v
	}
	if v, ok := m[Blocker].(bool); ok {
		rp.Blocker = v
	}
	if v, ok := m[Stored].(bool); ok {
		rp.Stored = v
	}
	rp.Weights = InterfaceToDynamicWeights(m[Weights])
	rp.ThresholdIDs = InterfaceToStringSlice(m[ThresholdIDs])
	return
}
