/*
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 (
	"errors"
	"fmt"
	"maps"
	"net/netip"
	"slices"
	"time"

	"github.com/cgrates/guardian"
)

// IPProfile defines the configuration of an IPAllocations object.
type IPProfile struct {
	Tenant    string
	ID        string
	FilterIDs []string
	Weights   DynamicWeights
	TTL       time.Duration
	Stored    bool
	Pools     []*IPPool

	lockID string // reference ID of lock used when matching the IPProfile
}

// IPProfileWithAPIOpts wraps IPProfile with APIOpts.
type IPProfileWithAPIOpts struct {
	*IPProfile
	APIOpts map[string]any
}

// TenantID returns the concatenated tenant and ID.
func (p *IPProfile) TenantID() string {
	return ConcatenatedKey(p.Tenant, p.ID)
}

// Clone creates a deep copy of IPProfile for thread-safe use.
func (p *IPProfile) Clone() *IPProfile {
	if p == nil {
		return nil
	}
	pools := make([]*IPPool, 0, len(p.Pools))
	for _, pool := range p.Pools {
		pools = append(pools, pool.Clone())
	}
	return &IPProfile{
		Tenant:    p.Tenant,
		ID:        p.ID,
		FilterIDs: slices.Clone(p.FilterIDs),
		Weights:   p.Weights.Clone(),
		TTL:       p.TTL,
		Stored:    p.Stored,
		Pools:     pools,
		lockID:    p.lockID,
	}
}

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

func (p *IPProfile) Set(path []string, val any, newBranch bool) error {
	if len(path) != 1 && len(path) != 2 {
		return ErrWrongPath
	}
	var err error
	switch path[0] {
	default:
		return ErrWrongPath
	case Tenant:
		p.Tenant = IfaceAsString(val)
	case ID:
		p.ID = IfaceAsString(val)
	case FilterIDs:
		var valA []string
		valA, err = IfaceAsStringSlice(val)
		p.FilterIDs = append(p.FilterIDs, valA...)
	case Weights:
		if val != "" {
			p.Weights, err = NewDynamicWeightsFromString(IfaceAsString(val), InfieldSep, ANDSep)
		}
	case TTL:
		p.TTL, err = IfaceAsDuration(val)
	case Stored:
		p.Stored, err = IfaceAsBool(val)
	case Pools:
		if len(path) != 2 {
			return ErrWrongPath
		}
		if val == "" {
			return nil
		}
		if len(p.Pools) == 0 || newBranch {
			p.Pools = append(p.Pools, new(IPPool))
		}
		pool := p.Pools[len(p.Pools)-1]
		return pool.Set(path[1:], val, newBranch)
	}
	return err
}

func (p *IPProfile) Merge(other any) {
	o := other.(*IPProfile)
	if len(o.Tenant) != 0 {
		p.Tenant = o.Tenant
	}
	if len(o.ID) != 0 {
		p.ID = o.ID
	}
	p.FilterIDs = append(p.FilterIDs, o.FilterIDs...)
	p.Weights = append(p.Weights, o.Weights...)
	if o.TTL != 0 {
		p.TTL = o.TTL
	}
	if o.Stored {
		p.Stored = o.Stored
	}
	for _, pool := range o.Pools {
		if idx := slices.IndexFunc(p.Pools, func(p *IPPool) bool {
			return p.ID == pool.ID
		}); idx != -1 {
			p.Pools[idx].Merge(pool)
			continue
		}
		p.Pools = append(p.Pools, pool)
	}
}

func (p *IPProfile) String() string { return ToJSON(p) }

func (p *IPProfile) FieldAsString(fldPath []string) (string, error) {
	val, err := p.FieldAsInterface(fldPath)
	if err != nil {
		return "", err
	}
	return IfaceAsString(val), nil
}

func (p *IPProfile) FieldAsInterface(fldPath []string) (any, error) {
	if len(fldPath) == 0 {
		return nil, ErrNotFound
	}
	switch fldPath[0] {
	default:
		fld, idx := GetPathIndex(fldPath[0])
		if idx != nil {
			switch fld {
			case FilterIDs:
				if *idx < len(p.FilterIDs) {
					return p.FilterIDs[*idx], nil
				}
			case Pools:
				if *idx < len(p.Pools) {
					return p.Pools[*idx].FieldAsInterface(fldPath[1:])
				}
			}
		}
		return nil, ErrNotFound
	case Tenant:
		return p.Tenant, nil
	case ID:
		return p.ID, nil
	case FilterIDs:
		return p.FilterIDs, nil
	case Weights:
		return p.Weights, nil
	case TTL:
		return p.TTL, nil
	case Stored:
		return p.Stored, nil
	case Pools:
		return p.Pools, nil
	}
}

// Lock acquires a guardian lock on the IPProfile and stores the lock ID.
// Uses given lockID or creates a new lock.
func (p *IPProfile) Lock(lockID string) {
	if lockID == "" {
		lockID = guardian.Guardian.GuardIDs("",
			0, // TODO: find a way to pass timeout without importing config
			IPProfileLockKey(p.Tenant, p.ID))
	}
	p.lockID = lockID
}

// Unlock releases the lock on the IPProfile and clears the stored lock ID.
func (p *IPProfile) Unlock() {
	if p.lockID == "" {
		return
	}

	// Store current lock ID before clearing to prevent race conditions.
	id := p.lockID
	p.lockID = ""
	guardian.Guardian.UnguardIDs(id)
}

// IPProfileLockKey returns the ID used to lock an IPProfile with guardian.
func IPProfileLockKey(tnt, id string) string {
	return ConcatenatedKey(CacheIPProfiles, tnt, id)
}

// IPPool defines a pool of IP addresses within an IPProfile.
type IPPool struct {
	ID        string
	FilterIDs []string
	Type      string
	Range     string
	Strategy  string
	Message   string
	Weights   DynamicWeights
	Blockers  DynamicBlockers
}

// Clone creates a deep copy of Pool for thread-safe use.
func (p *IPPool) Clone() *IPPool {
	if p == nil {
		return nil
	}
	return &IPPool{
		ID:        p.ID,
		FilterIDs: slices.Clone(p.FilterIDs),
		Type:      p.Type,
		Range:     p.Range,
		Strategy:  p.Strategy,
		Message:   p.Message,
		Weights:   p.Weights.Clone(),
		Blockers:  p.Blockers.Clone(),
	}
}

func (p *IPPool) Set(path []string, val any, _ bool) error {
	if len(path) != 1 {
		return ErrWrongPath
	}
	var err error
	switch path[0] {
	default:
		return ErrWrongPath
	case ID:
		p.ID = IfaceAsString(val)
	case FilterIDs:
		var valA []string
		valA, err = IfaceAsStringSlice(val)
		p.FilterIDs = append(p.FilterIDs, valA...)
	case Type:
		p.Type = IfaceAsString(val)
	case Range:
		p.Range = IfaceAsString(val)
	case Strategy:
		p.Strategy = IfaceAsString(val)
	case Message:
		p.Message = IfaceAsString(val)
	case Weights:
		if val != "" {
			p.Weights, err = NewDynamicWeightsFromString(IfaceAsString(val), InfieldSep, ANDSep)
		}
	case Blockers:
		if val != "" {
			p.Blockers, err = NewDynamicBlockersFromString(IfaceAsString(val), InfieldSep, ANDSep)
		}
	}
	return err
}

func (p *IPPool) Merge(other any) {
	o := other.(*IPPool)

	// NOTE: Merge gets called when the IDs are equal, so this is a no-op.
	// Kept for consistency with other components.
	if len(o.ID) != 0 {
		p.ID = o.ID
	}

	p.FilterIDs = append(p.FilterIDs, o.FilterIDs...)
	if o.Type != "" {
		p.Type = o.Type
	}
	if o.Range != "" {
		p.Range = o.Range
	}
	if o.Strategy != "" {
		p.Strategy = o.Strategy
	}
	if o.Message != "" {
		p.Message = o.Message
	}
	p.Weights = append(p.Weights, o.Weights...)
	p.Blockers = append(p.Blockers, o.Blockers...)
}

func (p *IPPool) String() string { return ToJSON(p) }

func (p *IPPool) FieldAsString(fldPath []string) (string, error) {
	val, err := p.FieldAsInterface(fldPath)
	if err != nil {
		return "", err
	}
	return IfaceAsString(val), nil
}

func (p *IPPool) FieldAsInterface(fldPath []string) (any, error) {
	if len(fldPath) == 0 {
		return p, nil
	}
	if len(fldPath) > 1 {
		return nil, ErrNotFound
	}
	switch fldPath[0] {
	default:
		fld, idx := GetPathIndex(fldPath[0])
		if idx != nil {
			switch fld {
			case FilterIDs:
				if *idx < len(p.FilterIDs) {
					return p.FilterIDs[*idx], nil
				}
			}
		}
		return nil, ErrNotFound
	case ID:
		return p.ID, nil
	case FilterIDs:
		return p.FilterIDs, nil
	case Type:
		return p.Type, nil
	case Range:
		return p.Range, nil
	case Strategy:
		return p.Strategy, nil
	case Message:
		return p.Message, nil
	case Weights:
		return p.Weights, nil
	case Blockers:
		return p.Blockers, nil
	}
}

// PoolAllocation represents one allocation in the pool.
type PoolAllocation struct {
	PoolID  string     // pool ID within the IPProfile
	Address netip.Addr // computed IP address
	Time    time.Time  // when this allocation was created
}

// IsActive checks if the allocation is still active.
func (a *PoolAllocation) IsActive(ttl time.Duration) bool {
	return time.Now().Before(a.Time.Add(ttl))
}

// Clone creates a deep copy of the PoolAllocation object.
func (a *PoolAllocation) Clone() *PoolAllocation {
	if a == nil {
		return nil
	}
	clone := *a
	return &clone
}

// AllocatedIP represents one IP allocated on a pool, together with the message.
type AllocatedIP struct {
	ProfileID string
	PoolID    string
	Message   string
	Address   netip.Addr
}

// AsNavigableMap implements engine.NavigableMapper.
func (ip *AllocatedIP) AsNavigableMap() map[string]*DataNode {
	return map[string]*DataNode{
		ProfileID: NewLeafNode(ip.ProfileID),
		PoolID:    NewLeafNode(ip.PoolID),
		Message:   NewLeafNode(ip.Message),
		Address:   NewLeafNode(ip.Address.String()),
	}
}

// IPAllocations represents IP allocations with usage tracking and TTL management.
type IPAllocations struct {
	Tenant      string
	ID          string
	Allocations map[string]*PoolAllocation // map[allocID]*PoolAllocation
	TTLIndex    []string                   // allocIDs ordered by allocation time for TTL expiry

	prfl       *IPProfile                       // cached profile configuration
	poolRanges map[string]netip.Prefix          // parsed CIDR ranges by pool ID
	poolAllocs map[string]map[netip.Addr]string // IP to allocation ID mapping by pool (map[poolID]map[Addr]allocID)
	lockID     string                           // guardian lock reference
}

// IPAllocationsWithAPIOpts wraps IPAllocations with APIOpts.
type IPAllocationsWithAPIOpts struct {
	*IPAllocations
	APIOpts map[string]any
}

// ClearIPAllocationsArgs contains arguments for clearing IP allocations.
// If AllocationIDs is empty or nil, all allocations will be cleared.
type ClearIPAllocationsArgs struct {
	Tenant        string
	ID            string
	AllocationIDs []string
	APIOpts       map[string]any
}

// ComputeUnexported sets up unexported fields based on the provided profile.
// Safe to call multiple times with the same profile.
func (a *IPAllocations) ComputeUnexported(prfl *IPProfile) error {
	if prfl == nil {
		return nil // nothing to compute without a profile
	}
	if a.prfl == prfl {
		return nil // already computed for this profile
	}
	a.prfl = prfl
	a.poolAllocs = make(map[string]map[netip.Addr]string)
	for allocID, alloc := range a.Allocations {
		if _, hasPool := a.poolAllocs[alloc.PoolID]; !hasPool {
			a.poolAllocs[alloc.PoolID] = make(map[netip.Addr]string)
		}
		a.poolAllocs[alloc.PoolID][alloc.Address] = allocID
	}
	a.poolRanges = make(map[string]netip.Prefix)
	for _, poolCfg := range a.prfl.Pools {
		prefix, err := netip.ParsePrefix(poolCfg.Range)
		if err != nil {
			return err
		}
		a.poolRanges[poolCfg.ID] = prefix
	}
	return nil
}

// ReleaseAllocation releases the allocation for an ID.
func (a *IPAllocations) ReleaseAllocation(allocID string) error {
	alloc, has := a.Allocations[allocID] // Get the allocation first
	if !has {
		return fmt.Errorf("cannot find allocation record with id: %s", allocID)
	}
	if poolMap, hasPool := a.poolAllocs[alloc.PoolID]; hasPool {
		delete(poolMap, alloc.Address)
	}
	if a.prfl.TTL > 0 {
		for i, refID := range a.TTLIndex {
			if refID == allocID {
				a.TTLIndex = slices.Delete(a.TTLIndex, i, i+1)
				break
			}
		}
	}
	delete(a.Allocations, allocID)
	return nil
}

// ClearAllocations clears specified IP allocations or all allocations if allocIDs is empty/nil.
// Either all specified IDs exist and get cleared, or none are cleared and an error is returned.
func (a *IPAllocations) ClearAllocations(allocIDs []string) error {
	if len(allocIDs) == 0 {
		clear(a.Allocations)
		clear(a.poolAllocs)
		a.TTLIndex = a.TTLIndex[:0] // maintain capacity
		return nil
	}

	// Validate all IDs exist before clearing any.
	var notFound []string
	for _, allocID := range allocIDs {
		if _, has := a.Allocations[allocID]; !has {
			notFound = append(notFound, allocID)
		}
	}
	if len(notFound) > 0 {
		return fmt.Errorf("cannot find allocation records with ids: %v", notFound)
	}

	for _, allocID := range allocIDs {
		alloc := a.Allocations[allocID]
		if poolMap, hasPool := a.poolAllocs[alloc.PoolID]; hasPool {
			delete(poolMap, alloc.Address)
		}
		if a.prfl.TTL > 0 {
			for i, refID := range a.TTLIndex {
				if refID == allocID {
					a.TTLIndex = slices.Delete(a.TTLIndex, i, i+1)
					break
				}
			}
		}
		delete(a.Allocations, allocID)
	}

	return nil
}

// AllocateIPOnPool allocates an IP from the specified pool or refreshes
// existing allocation. If dryRun is true, checks availability without
// allocating.
func (a *IPAllocations) AllocateIPOnPool(allocID string, pool *IPPool,
	dryRun bool) (*AllocatedIP, error) {
	a.removeExpiredUnits()
	if poolAlloc, has := a.Allocations[allocID]; has && !dryRun {
		poolAlloc.Time = time.Now()
		if a.prfl.TTL > 0 {
			a.removeAllocFromTTLIndex(allocID)
		}
		a.TTLIndex = append(a.TTLIndex, allocID)
		return &AllocatedIP{
			ProfileID: a.ID,
			PoolID:    pool.ID,
			Message:   pool.Message,
			Address:   poolAlloc.Address,
		}, nil
	}
	poolRange := a.poolRanges[pool.ID]
	if !poolRange.IsSingleIP() {
		return nil, errors.New("only single IP Pools are supported for now")
	}
	addr := poolRange.Addr()
	if _, hasPool := a.poolAllocs[pool.ID]; hasPool {
		if alcID, inUse := a.poolAllocs[pool.ID][addr]; inUse {
			return nil, fmt.Errorf("allocation failed for pool %q, IP %q: %w (allocated to %q)",
				pool.ID, addr, ErrIPAlreadyAllocated, alcID)
		}
	}
	allocIP := &AllocatedIP{
		ProfileID: a.ID,
		PoolID:    pool.ID,
		Message:   pool.Message,
		Address:   addr,
	}
	if dryRun {
		return allocIP, nil
	}
	a.Allocations[allocID] = &PoolAllocation{
		PoolID:  pool.ID,
		Address: addr,
		Time:    time.Now(),
	}
	if _, hasPool := a.poolAllocs[pool.ID]; !hasPool {
		a.poolAllocs[pool.ID] = make(map[netip.Addr]string)
	}
	a.poolAllocs[pool.ID][addr] = allocID
	return allocIP, nil
}

// removeExpiredUnits removes expired allocations.
// It stops at first active since TTLIndex is sorted by expiration.
func (a *IPAllocations) removeExpiredUnits() {
	expiredCount := 0
	for _, allocID := range a.TTLIndex {
		alloc, exists := a.Allocations[allocID]
		if exists && alloc.IsActive(a.prfl.TTL) {
			break
		}
		if alloc != nil {
			if poolMap, hasPool := a.poolAllocs[alloc.PoolID]; hasPool {
				delete(poolMap, alloc.Address)
			}
		}
		delete(a.Allocations, allocID)
		expiredCount++
	}
	if expiredCount > 0 {
		a.TTLIndex = a.TTLIndex[expiredCount:]
	}
}

// removeAllocFromTTLIndex removes an allocationID from TTL index.
func (a *IPAllocations) removeAllocFromTTLIndex(allocID string) {
	for i, alID := range a.TTLIndex {
		if alID == allocID {
			a.TTLIndex = slices.Delete(a.TTLIndex, i, i+1)
			break
		}
	}
}

// Lock acquires a guardian lock on the IPAllocations and stores the lock ID.
// Uses given lockID (assumes already acquired) or creates a new lock.
func (a *IPAllocations) Lock(lockID string) {
	if lockID == "" {
		lockID = guardian.Guardian.GuardIDs("",
			0, // TODO: find a way to pass timeout without importing config
			IPAllocationsLockKey(a.Tenant, a.ID))
	}
	a.lockID = lockID
}

// Unlock releases the lock on the IPAllocations and clears the stored lock ID.
func (a *IPAllocations) Unlock() {
	if a.lockID == "" {
		return
	}

	// Store current lock ID before clearing to prevent race conditions.
	id := a.lockID
	a.lockID = ""
	guardian.Guardian.UnguardIDs(id)

	if a.prfl != nil {
		a.prfl.Unlock()
	}
}

// Config returns the IPAllocations' profile configuration.
func (a *IPAllocations) Config() *IPProfile {
	return a.prfl
}

// AsMapStringInterface converts IPProfile struct to map[string]any
func (p *IPProfile) AsMapStringInterface() map[string]any {
	if p == nil {
		return nil
	}
	return map[string]any{
		Tenant:    p.Tenant,
		ID:        p.ID,
		FilterIDs: p.FilterIDs,
		Weights:   p.Weights,
		TTL:       p.TTL,
		Stored:    p.Stored,
		Pools:     p.Pools,
	}
}

// MapStringInterfaceToIPProfile converts map[string]any to IPProfile struct
func MapStringInterfaceToIPProfile(m map[string]any) (*IPProfile, error) {
	ipp := &IPProfile{}

	if v, ok := m[Tenant].(string); ok {
		ipp.Tenant = v
	}
	if v, ok := m[ID].(string); ok {
		ipp.ID = v
	}
	ipp.FilterIDs = InterfaceToStringSlice(m[FilterIDs])
	ipp.Weights = InterfaceToDynamicWeights(m[Weights])
	if v, ok := m[TTL].(string); ok {
		if dur, err := time.ParseDuration(v); err != nil {
			return nil, err
		} else {
			ipp.TTL = dur
		}
	} else if v, ok := m[TTL].(float64); ok { // for -1 cases
		ipp.TTL = time.Duration(v)
	}
	if v, ok := m[Stored].(bool); ok {
		ipp.Stored = v
	}
	ipp.Pools = InterfaceToPools(m[Pools])
	return ipp, nil
}

// InterfaceToPools converts any to []*IPPool
func InterfaceToPools(v any) []*IPPool {
	if v == nil {
		return nil
	}
	if pools, ok := v.([]any); ok {
		ipPools := make([]*IPPool, 0, len(pools))
		for _, p := range pools {
			pm, ok := p.(map[string]any)
			if !ok {
				break
			}
			pool := &IPPool{}
			if v, ok := pm[ID].(string); ok {
				pool.ID = v
			}

			pool.FilterIDs = InterfaceToStringSlice(pm[FilterIDs])
			if v, ok := pm[Type].(string); ok {
				pool.Type = v
			}
			if v, ok := pm[Range].(string); ok {
				pool.Range = v
			}
			if v, ok := pm[Strategy].(string); ok {
				pool.Strategy = v
			}
			if v, ok := pm[Message].(string); ok {
				pool.Message = v
			}
			pool.Weights = InterfaceToDynamicWeights(pm[Weights])
			pool.Blockers = InterfaceToDynamicBlockers(pm[Blockers])
			ipPools = append(ipPools, pool)
		}
		return ipPools
	}
	return nil
}

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

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

// Clone creates a deep clone of the IPAllocations object (lockID excluded).
func (a *IPAllocations) Clone() *IPAllocations {
	if a == nil {
		return nil
	}
	clone := &IPAllocations{
		Tenant:     a.Tenant,
		ID:         a.ID,
		TTLIndex:   slices.Clone(a.TTLIndex),
		prfl:       a.prfl.Clone(),
		poolRanges: maps.Clone(a.poolRanges),
	}
	if a.poolAllocs != nil {
		clone.poolAllocs = make(map[string]map[netip.Addr]string)
		for poolID, allocs := range a.poolAllocs {
			clone.poolAllocs[poolID] = maps.Clone(allocs)
		}
	}
	if a.Allocations != nil {
		clone.Allocations = make(map[string]*PoolAllocation, len(a.Allocations))
		for id, alloc := range a.Allocations {
			clone.Allocations[id] = alloc.Clone()
		}
	}
	return clone
}

// IPAllocationsLockKey builds the guardian key for locking IP allocations.
func IPAllocationsLockKey(tnt, id string) string {
	return ConcatenatedKey(CacheIPAllocations, tnt, id)
}

// AsMapStringInterface converts IPAllocations struct to map[string]any
func (p *IPAllocations) AsMapStringInterface() map[string]any {
	if p == nil {
		return nil
	}
	return map[string]any{
		Tenant:      p.Tenant,
		ID:          p.ID,
		Allocations: p.Allocations,
		TTLIndex:    p.TTLIndex,
	}
}

// MapStringInterfaceToIPAllocations converts map[string]any to IPAllocations struct
func MapStringInterfaceToIPAllocations(m map[string]any) *IPAllocations {
	ipa := &IPAllocations{}

	if v, ok := m[Tenant].(string); ok {
		ipa.Tenant = v
	}
	if v, ok := m[ID].(string); ok {
		ipa.ID = v
	}
	ipa.Allocations = InterfaceToAllocations(m[Allocations])
	ipa.TTLIndex = InterfaceToStringSlice(m[TTLIndex])
	return ipa
}

// InterfaceToAllocations converts any to map[string]*PoolAllocation
func InterfaceToAllocations(v any) map[string]*PoolAllocation {
	if v == nil {
		return nil
	}
	if allocs, ok := v.(map[string]any); ok {
		ipAllocs := make(map[string]*PoolAllocation)
		for allocID, val := range allocs {
			allocMap, ok := val.(map[string]any)
			if !ok {
				break
			}
			allocation := &PoolAllocation{}
			if v, ok := allocMap[PoolID].(string); ok {
				allocation.PoolID = v
			}
			if v, ok := allocMap[Address].(string); ok {
				if addr, err := netip.ParseAddr(v); err == nil {
					allocation.Address = addr
				}
			}
			if v, ok := allocMap[Time].(string); ok {
				if t, err := time.Parse(time.RFC3339, v); err == nil {
					allocation.Time = t
				}
			}
			ipAllocs[allocID] = allocation
		}
		return ipAllocs
	}
	return nil
}
