package discovery

import (
	"fmt"
	"strings"
	"sync"
	"time"
)

const (
	DefaultHealthIntervalSec = 10
	DefaultHealthTimeoutSec  = 30
)

type RegistryEntry struct {
	ID              string                 `json:"id"`
	InstanceID      string                 `bson:"instanceId" json:"instanceId"`
	Service         string                 `json:"service"`
	Rail            string                 `json:"rail,omitempty"`
	Network         string                 `json:"network,omitempty"`
	Operations      []string               `json:"operations,omitempty"`
	Currencies      []string               `json:"currencies,omitempty"`
	CurrencyMeta    []CurrencyAnnouncement `json:"currencyMeta,omitempty"`
	Limits          *Limits                `json:"limits,omitempty"`
	InvokeURI       string                 `json:"invokeURI,omitempty"`
	RoutingPriority int                    `json:"routingPriority,omitempty"`
	Version         string                 `json:"version,omitempty"`
	Health          HealthParams           `json:"health,omitempty"`
	LastHeartbeat   time.Time              `json:"lastHeartbeat,omitempty"`
	Status          string                 `json:"status,omitempty"`
	Healthy         bool                   `json:"healthy,omitempty"`
}

type Registry struct {
	mu         sync.RWMutex
	entries    map[string]*RegistryEntry
	byID       map[string]map[string]struct{}
	byInstance map[string]map[string]struct{}
}

type UpdateResult struct {
	Entry         RegistryEntry
	IsNew         bool
	WasHealthy    bool
	BecameHealthy bool
}

func NewRegistry() *Registry {
	return &Registry{
		entries:    map[string]*RegistryEntry{},
		byID:       map[string]map[string]struct{}{},
		byInstance: map[string]map[string]struct{}{},
	}
}

func (r *Registry) UpsertFromAnnouncement(announce Announcement, now time.Time) UpdateResult {
	entry := registryEntryFromAnnouncement(normalizeAnnouncement(announce), now)
	key := registryEntryKey(entry)
	if key == "" {
		return UpdateResult{Entry: entry}
	}

	r.mu.Lock()
	defer r.mu.Unlock()

	existing, ok := r.entries[key]
	wasHealthy := false
	if ok && existing != nil {
		wasHealthy = existing.isHealthyAt(now)
		r.unindexEntry(key, existing)
	}
	entry.Healthy = entry.isHealthyAt(now)
	r.entries[key] = &entry
	r.indexEntry(key, &entry)

	return UpdateResult{
		Entry:         entry,
		IsNew:         !ok,
		WasHealthy:    wasHealthy,
		BecameHealthy: !wasHealthy && entry.Healthy,
	}
}

func (r *Registry) UpsertEntry(entry RegistryEntry, now time.Time) UpdateResult {
	entry = normalizeEntry(entry)
	key := registryEntryKey(entry)
	if key == "" {
		return UpdateResult{Entry: entry}
	}
	if entry.LastHeartbeat.IsZero() {
		entry.LastHeartbeat = now
	}
	if strings.TrimSpace(entry.Status) == "" {
		entry.Status = "ok"
	}

	r.mu.Lock()
	defer r.mu.Unlock()

	existing, ok := r.entries[key]
	wasHealthy := false
	if ok && existing != nil {
		wasHealthy = existing.isHealthyAt(now)
		r.unindexEntry(key, existing)
	}
	entry.Healthy = entry.isHealthyAt(now)
	r.entries[key] = &entry
	r.indexEntry(key, &entry)

	return UpdateResult{
		Entry:         entry,
		IsNew:         !ok,
		WasHealthy:    wasHealthy,
		BecameHealthy: !wasHealthy && entry.Healthy,
	}
}

func (r *Registry) UpdateHeartbeat(id string, instanceID string, status string, ts time.Time, now time.Time) []UpdateResult {
	id = strings.TrimSpace(id)
	instanceID = strings.TrimSpace(instanceID)
	if id == "" && instanceID == "" {
		return nil
	}
	if status == "" {
		status = "ok"
	}
	if ts.IsZero() {
		ts = now
	}

	r.mu.Lock()
	defer r.mu.Unlock()

	keys := keysFromIndex(r.byInstance[instanceID])
	if len(keys) == 0 && id != "" {
		keys = keysFromIndex(r.byID[id])
	}
	if len(keys) == 0 {
		return nil
	}
	results := make([]UpdateResult, 0, len(keys))
	for _, key := range keys {
		entry := r.entries[key]
		if entry == nil {
			continue
		}
		if id != "" && entry.ID != id {
			continue
		}
		if instanceID != "" && entry.InstanceID != instanceID {
			continue
		}
		results = append(results, updateHeartbeatEntry(entry, status, ts, now))
	}
	return results
}

func (r *Registry) UpdateHeartbeatByKey(key string, status string, ts time.Time, now time.Time) (UpdateResult, bool) {
	key = strings.TrimSpace(key)
	if key == "" {
		return UpdateResult{}, false
	}
	if status == "" {
		status = "ok"
	}
	if ts.IsZero() {
		ts = now
	}

	r.mu.Lock()
	defer r.mu.Unlock()

	entry := r.entries[key]
	if entry == nil {
		return UpdateResult{}, false
	}

	return updateHeartbeatEntry(entry, status, ts, now), true
}

func (r *Registry) Delete(key string) bool {
	key = strings.TrimSpace(key)
	if key == "" {
		return false
	}
	r.mu.Lock()
	defer r.mu.Unlock()
	entry, ok := r.entries[key]
	if !ok {
		return false
	}
	delete(r.entries, key)
	r.unindexEntry(key, entry)
	return true
}

func updateHeartbeatEntry(entry *RegistryEntry, status string, ts time.Time, now time.Time) UpdateResult {
	wasHealthy := entry.isHealthyAt(now)
	entry.Status = status
	entry.LastHeartbeat = ts
	entry.Healthy = entry.isHealthyAt(now)

	return UpdateResult{
		Entry:         *entry,
		IsNew:         false,
		WasHealthy:    wasHealthy,
		BecameHealthy: !wasHealthy && entry.Healthy,
	}
}

// DeleteStale removes entries whose heartbeat timeout has elapsed.
func (r *Registry) DeleteStale(now time.Time) []string {
	r.mu.Lock()
	defer r.mu.Unlock()

	removed := make([]string, 0)
	for key, entry := range r.entries {
		if entry == nil || !entry.isStaleAt(now) {
			continue
		}
		delete(r.entries, key)
		r.unindexEntry(key, entry)
		removed = append(removed, key)
	}
	if len(removed) == 0 {
		return nil
	}
	return removed
}

func (r *Registry) List(now time.Time, onlyHealthy bool) []RegistryEntry {
	r.mu.Lock()
	defer r.mu.Unlock()

	result := make([]RegistryEntry, 0, len(r.entries))
	for _, entry := range r.entries {
		if entry == nil {
			continue
		}
		entry.Healthy = entry.isHealthyAt(now)
		if onlyHealthy && !entry.Healthy {
			continue
		}
		cp := *entry
		result = append(result, cp)
	}
	return result
}

func registryEntryFromAnnouncement(announce Announcement, now time.Time) RegistryEntry {
	status := "ok"
	currencies := cloneCurrencyAnnouncements(announce.Currencies)
	return RegistryEntry{
		ID:              strings.TrimSpace(announce.ID),
		InstanceID:      strings.TrimSpace(announce.InstanceID),
		Service:         strings.TrimSpace(announce.Service),
		Rail:            strings.ToUpper(strings.TrimSpace(announce.Rail)),
		Network:         legacyNetworkFromCurrencies(currencies),
		Operations:      cloneStrings(announce.Operations),
		Currencies:      legacyCurrencyCodes(currencies),
		CurrencyMeta:    currencies,
		Limits:          legacyLimitsFromCurrencies(currencies),
		InvokeURI:       strings.TrimSpace(announce.InvokeURI),
		RoutingPriority: announce.RoutingPriority,
		Version:         strings.TrimSpace(announce.Version),
		Health:          normalizeHealth(announce.Health),
		LastHeartbeat:   now,
		Status:          status,
	}
}

func normalizeEntry(entry RegistryEntry) RegistryEntry {
	entry.ID = strings.TrimSpace(entry.ID)
	entry.InstanceID = strings.TrimSpace(entry.InstanceID)
	if entry.InstanceID == "" {
		entry.InstanceID = entry.ID
	}
	entry.Service = strings.TrimSpace(entry.Service)
	entry.Rail = NormalizeRail(entry.Rail)
	entry.Network = strings.ToUpper(strings.TrimSpace(entry.Network))
	entry.Operations = normalizeStrings(entry.Operations, false)
	entry.CurrencyMeta = normalizeCurrencyAnnouncements(entry.CurrencyMeta)
	if len(entry.CurrencyMeta) > 0 {
		entry.Currencies = legacyCurrencyCodes(entry.CurrencyMeta)
		if derivedNetwork := legacyNetworkFromCurrencies(entry.CurrencyMeta); derivedNetwork != "" {
			entry.Network = derivedNetwork
		}
		entry.Limits = legacyLimitsFromCurrencies(entry.CurrencyMeta)
	} else {
		entry.Currencies = normalizeStrings(entry.Currencies, true)
	}
	entry.InvokeURI = strings.TrimSpace(entry.InvokeURI)
	entry.Version = strings.TrimSpace(entry.Version)
	entry.Status = strings.TrimSpace(entry.Status)
	entry.Health = normalizeHealth(entry.Health)
	if len(entry.CurrencyMeta) == 0 && entry.Limits != nil {
		entry.Limits = normalizeLimits(*entry.Limits)
	}
	return entry
}

func normalizeAnnouncement(announce Announcement) Announcement {
	announce.ID = strings.TrimSpace(announce.ID)
	announce.InstanceID = strings.TrimSpace(announce.InstanceID)
	if announce.InstanceID == "" {
		announce.InstanceID = announce.ID
	}
	announce.Service = strings.TrimSpace(announce.Service)
	announce.Rail = NormalizeRail(announce.Rail)
	announce.Operations = normalizeStrings(announce.Operations, false)
	announce.Currencies = normalizeCurrencyAnnouncements(announce.Currencies)
	announce.InvokeURI = strings.TrimSpace(announce.InvokeURI)
	announce.Version = strings.TrimSpace(announce.Version)
	announce.Health = normalizeHealth(announce.Health)
	return announce
}

func normalizeHealth(h HealthParams) HealthParams {
	if h.IntervalSec <= 0 {
		h.IntervalSec = DefaultHealthIntervalSec
	}
	if h.TimeoutSec <= 0 {
		h.TimeoutSec = DefaultHealthTimeoutSec
	}
	if h.TimeoutSec < h.IntervalSec {
		h.TimeoutSec = h.IntervalSec * 2
	}
	return h
}

func normalizeLimits(l Limits) *Limits {
	res := l
	if len(res.VolumeLimit) == 0 {
		res.VolumeLimit = nil
	}
	if len(res.VelocityLimit) == 0 {
		res.VelocityLimit = nil
	}
	return &res
}

func cloneLimits(src *Limits) *Limits {
	if src == nil {
		return nil
	}
	dst := *src
	if src.VolumeLimit != nil {
		dst.VolumeLimit = map[string]string{}
		for key, value := range src.VolumeLimit {
			if strings.TrimSpace(key) == "" {
				continue
			}
			dst.VolumeLimit[strings.TrimSpace(key)] = strings.TrimSpace(value)
		}
	}
	if src.VelocityLimit != nil {
		dst.VelocityLimit = map[string]int{}
		for key, value := range src.VelocityLimit {
			if strings.TrimSpace(key) == "" {
				continue
			}
			dst.VelocityLimit[strings.TrimSpace(key)] = value
		}
	}
	return &dst
}

func normalizeCurrencyAnnouncements(values []CurrencyAnnouncement) []CurrencyAnnouncement {
	if len(values) == 0 {
		return nil
	}
	seen := map[string]bool{}
	result := make([]CurrencyAnnouncement, 0, len(values))
	for _, value := range values {
		clean, ok := normalizeCurrencyAnnouncement(value)
		if !ok {
			continue
		}
		key := strings.Join([]string{
			clean.Currency,
			clean.Network,
			clean.ProviderID,
			clean.ContractAddress,
		}, "|")
		if seen[key] {
			continue
		}
		seen[key] = true
		result = append(result, clean)
	}
	if len(result) == 0 {
		return nil
	}
	return result
}

func normalizeCurrencyAnnouncement(src CurrencyAnnouncement) (CurrencyAnnouncement, bool) {
	src.Currency = strings.ToUpper(strings.TrimSpace(src.Currency))
	if src.Currency == "" {
		return CurrencyAnnouncement{}, false
	}
	src.Network = strings.ToUpper(strings.TrimSpace(src.Network))
	src.ProviderID = strings.TrimSpace(src.ProviderID)
	src.ContractAddress = strings.ToLower(strings.TrimSpace(src.ContractAddress))
	if src.Decimals != nil && *src.Decimals < 0 {
		src.Decimals = nil
	}
	src.Limits = normalizeCurrencyLimits(src.Limits)
	return src, true
}

func normalizeCurrencyLimits(src *CurrencyLimits) *CurrencyLimits {
	if src == nil {
		return nil
	}
	dst := &CurrencyLimits{}
	if src.Amount != nil {
		amount := &CurrencyAmount{
			Min: strings.TrimSpace(src.Amount.Min),
			Max: strings.TrimSpace(src.Amount.Max),
		}
		if amount.Min != "" || amount.Max != "" {
			dst.Amount = amount
		}
	}
	if src.Running != nil {
		running := &CurrencyRunningLimits{}
		for _, limit := range src.Running.Volume {
			max := strings.TrimSpace(limit.Max)
			if max == "" {
				continue
			}
			window := normalizeWindow(limit.Window)
			if legacyWindowKey(window) == "" {
				continue
			}
			running.Volume = append(running.Volume, VolumeLimit{
				Window: window,
				Max:    max,
			})
		}
		for _, limit := range src.Running.Velocity {
			if limit.Max <= 0 {
				continue
			}
			window := normalizeWindow(limit.Window)
			if legacyWindowKey(window) == "" {
				continue
			}
			running.Velocity = append(running.Velocity, VelocityLimit{
				Window: window,
				Max:    limit.Max,
			})
		}
		if len(running.Volume) > 0 || len(running.Velocity) > 0 {
			dst.Running = running
		}
	}
	if dst.Amount == nil && dst.Running == nil {
		return nil
	}
	return dst
}

func normalizeWindow(src Window) Window {
	src.Raw = strings.TrimSpace(src.Raw)
	src.Duration = strings.TrimSpace(src.Duration)
	src.Named = strings.TrimSpace(src.Named)
	if src.Calendar != nil {
		cal := &CalendarWindow{
			Unit:  CalendarUnit(strings.ToLower(strings.TrimSpace(string(src.Calendar.Unit)))),
			Count: src.Calendar.Count,
		}
		if cal.Count <= 0 {
			cal.Count = 1
		}
		if cal.Unit == CalendarUnitUnspecified {
			cal = nil
		}
		src.Calendar = cal
	}
	return src
}

func cloneCurrencyAnnouncements(values []CurrencyAnnouncement) []CurrencyAnnouncement {
	if len(values) == 0 {
		return nil
	}
	out := make([]CurrencyAnnouncement, 0, len(values))
	for _, value := range values {
		cp := CurrencyAnnouncement{
			Currency:        value.Currency,
			Network:         value.Network,
			ProviderID:      value.ProviderID,
			ContractAddress: value.ContractAddress,
		}
		if value.Decimals != nil {
			decimals := *value.Decimals
			cp.Decimals = &decimals
		}
		cp.Limits = cloneCurrencyLimits(value.Limits)
		out = append(out, cp)
	}
	return out
}

func cloneCurrencyLimits(src *CurrencyLimits) *CurrencyLimits {
	if src == nil {
		return nil
	}
	dst := &CurrencyLimits{}
	if src.Amount != nil {
		dst.Amount = &CurrencyAmount{
			Min: src.Amount.Min,
			Max: src.Amount.Max,
		}
	}
	if src.Running != nil {
		running := &CurrencyRunningLimits{}
		if len(src.Running.Volume) > 0 {
			running.Volume = make([]VolumeLimit, 0, len(src.Running.Volume))
			for _, item := range src.Running.Volume {
				running.Volume = append(running.Volume, VolumeLimit{
					Window: cloneWindow(item.Window),
					Max:    item.Max,
				})
			}
		}
		if len(src.Running.Velocity) > 0 {
			running.Velocity = make([]VelocityLimit, 0, len(src.Running.Velocity))
			for _, item := range src.Running.Velocity {
				running.Velocity = append(running.Velocity, VelocityLimit{
					Window: cloneWindow(item.Window),
					Max:    item.Max,
				})
			}
		}
		if len(running.Volume) > 0 || len(running.Velocity) > 0 {
			dst.Running = running
		}
	}
	if dst.Amount == nil && dst.Running == nil {
		return nil
	}
	return dst
}

func cloneWindow(src Window) Window {
	dst := Window{
		Raw:      src.Raw,
		Duration: src.Duration,
		Named:    src.Named,
	}
	if src.Calendar != nil {
		dst.Calendar = &CalendarWindow{
			Unit:  src.Calendar.Unit,
			Count: src.Calendar.Count,
		}
	}
	return dst
}

func legacyCurrencyCodes(values []CurrencyAnnouncement) []string {
	if len(values) == 0 {
		return nil
	}
	seen := map[string]bool{}
	out := make([]string, 0, len(values))
	for _, value := range values {
		currency := strings.ToUpper(strings.TrimSpace(value.Currency))
		if currency == "" || seen[currency] {
			continue
		}
		seen[currency] = true
		out = append(out, currency)
	}
	if len(out) == 0 {
		return nil
	}
	return out
}

func legacyNetworkFromCurrencies(values []CurrencyAnnouncement) string {
	if len(values) == 0 {
		return ""
	}
	network := ""
	for _, value := range values {
		current := strings.ToUpper(strings.TrimSpace(value.Network))
		if current == "" {
			continue
		}
		if network == "" {
			network = current
			continue
		}
		if network != current {
			return ""
		}
	}
	return network
}

func legacyLimitsFromCurrencies(values []CurrencyAnnouncement) *Limits {
	if len(values) == 0 {
		return nil
	}
	var merged *Limits
	for _, value := range values {
		current := legacyLimitsFromCurrency(value.Limits)
		if current == nil {
			continue
		}
		if merged == nil {
			merged = current
			continue
		}
		if !strings.EqualFold(strings.TrimSpace(merged.MinAmount), strings.TrimSpace(current.MinAmount)) {
			merged.MinAmount = ""
		}
		if !strings.EqualFold(strings.TrimSpace(merged.MaxAmount), strings.TrimSpace(current.MaxAmount)) {
			merged.MaxAmount = ""
		}
		merged.VolumeLimit = intersectStringMaps(merged.VolumeLimit, current.VolumeLimit)
		merged.VelocityLimit = intersectIntMaps(merged.VelocityLimit, current.VelocityLimit)
	}
	if merged == nil {
		return nil
	}
	if merged.MinAmount == "" && merged.MaxAmount == "" && len(merged.VolumeLimit) == 0 && len(merged.VelocityLimit) == 0 {
		return nil
	}
	return merged
}

func legacyLimitsFromCurrency(src *CurrencyLimits) *Limits {
	if src == nil {
		return nil
	}
	out := &Limits{}
	if src.Amount != nil {
		out.MinAmount = strings.TrimSpace(src.Amount.Min)
		out.MaxAmount = strings.TrimSpace(src.Amount.Max)
	}
	if src.Running != nil {
		if len(src.Running.Volume) > 0 {
			out.VolumeLimit = map[string]string{}
			for _, item := range src.Running.Volume {
				key := legacyWindowKey(item.Window)
				max := strings.TrimSpace(item.Max)
				if key == "" || max == "" {
					continue
				}
				out.VolumeLimit[key] = max
			}
		}
		if len(src.Running.Velocity) > 0 {
			out.VelocityLimit = map[string]int{}
			for _, item := range src.Running.Velocity {
				key := legacyWindowKey(item.Window)
				if key == "" || item.Max <= 0 {
					continue
				}
				out.VelocityLimit[key] = item.Max
			}
		}
	}
	if out.MinAmount == "" && out.MaxAmount == "" && len(out.VolumeLimit) == 0 && len(out.VelocityLimit) == 0 {
		return nil
	}
	return out
}

func intersectStringMaps(left, right map[string]string) map[string]string {
	if len(left) == 0 || len(right) == 0 {
		return nil
	}
	out := map[string]string{}
	for key, value := range left {
		if rightValue, ok := right[key]; ok && strings.EqualFold(strings.TrimSpace(value), strings.TrimSpace(rightValue)) {
			out[key] = value
		}
	}
	if len(out) == 0 {
		return nil
	}
	return out
}

func intersectIntMaps(left, right map[string]int) map[string]int {
	if len(left) == 0 || len(right) == 0 {
		return nil
	}
	out := map[string]int{}
	for key, value := range left {
		if rightValue, ok := right[key]; ok && value == rightValue {
			out[key] = value
		}
	}
	if len(out) == 0 {
		return nil
	}
	return out
}

func legacyWindowKey(window Window) string {
	if raw := strings.TrimSpace(window.Raw); raw != "" {
		return raw
	}
	if named := strings.TrimSpace(window.Named); named != "" {
		return named
	}
	if duration := strings.TrimSpace(window.Duration); duration != "" {
		return duration
	}
	if window.Calendar != nil && window.Calendar.Unit != CalendarUnitUnspecified {
		count := window.Calendar.Count
		if count <= 0 {
			count = 1
		}
		return fmt.Sprintf("%d%s", count, strings.ToLower(strings.TrimSpace(string(window.Calendar.Unit))))
	}
	return ""
}

func normalizeStrings(values []string, upper bool) []string {
	if len(values) == 0 {
		return nil
	}
	seen := map[string]bool{}
	result := make([]string, 0, len(values))
	for _, value := range values {
		clean := strings.TrimSpace(value)
		if clean == "" {
			continue
		}
		if upper {
			clean = strings.ToUpper(clean)
		}
		if seen[clean] {
			continue
		}
		seen[clean] = true
		result = append(result, clean)
	}
	if len(result) == 0 {
		return nil
	}
	return result
}

func cloneStrings(values []string) []string {
	if len(values) == 0 {
		return nil
	}
	out := make([]string, len(values))
	copy(out, values)
	return out
}

func (r *Registry) indexEntry(key string, entry *RegistryEntry) {
	if r == nil || entry == nil || key == "" {
		return
	}
	if entry.ID != "" {
		addIndex(r.byID, entry.ID, key)
	}
	if entry.InstanceID != "" {
		addIndex(r.byInstance, entry.InstanceID, key)
	}
}

func (r *Registry) unindexEntry(key string, entry *RegistryEntry) {
	if r == nil || entry == nil || key == "" {
		return
	}
	if entry.ID != "" {
		removeIndex(r.byID, entry.ID, key)
	}
	if entry.InstanceID != "" {
		removeIndex(r.byInstance, entry.InstanceID, key)
	}
}

func addIndex(index map[string]map[string]struct{}, id string, key string) {
	if id == "" || key == "" {
		return
	}
	set := index[id]
	if set == nil {
		set = map[string]struct{}{}
		index[id] = set
	}
	set[key] = struct{}{}
}

func removeIndex(index map[string]map[string]struct{}, id string, key string) {
	if id == "" || key == "" {
		return
	}
	set := index[id]
	if set == nil {
		return
	}
	delete(set, key)
	if len(set) == 0 {
		delete(index, id)
	}
}

func keysFromIndex(index map[string]struct{}) []string {
	if len(index) == 0 {
		return nil
	}
	keys := make([]string, 0, len(index))
	for key := range index {
		keys = append(keys, key)
	}
	return keys
}

func (e *RegistryEntry) isHealthyAt(now time.Time) bool {
	if e == nil {
		return false
	}
	status := strings.ToLower(strings.TrimSpace(e.Status))
	if status != "" && status != "ok" {
		return false
	}
	if e.LastHeartbeat.IsZero() {
		return false
	}
	timeout := time.Duration(e.Health.TimeoutSec) * time.Second
	if timeout <= 0 {
		timeout = time.Duration(DefaultHealthTimeoutSec) * time.Second
	}
	return now.Sub(e.LastHeartbeat) <= timeout
}

func (e *RegistryEntry) isStaleAt(now time.Time) bool {
	if e == nil {
		return false
	}
	if e.LastHeartbeat.IsZero() {
		return true
	}
	timeout := time.Duration(e.Health.TimeoutSec) * time.Second
	if timeout <= 0 {
		timeout = time.Duration(DefaultHealthTimeoutSec) * time.Second
	}
	return now.Sub(e.LastHeartbeat) > timeout
}
