package discovery

import (
	"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"`
	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
		}
		wasHealthy := entry.isHealthyAt(now)
		entry.Status = status
		entry.LastHeartbeat = ts
		entry.Healthy = entry.isHealthyAt(now)

		results = append(results, UpdateResult{
			Entry:         *entry,
			IsNew:         false,
			WasHealthy:    wasHealthy,
			BecameHealthy: !wasHealthy && entry.Healthy,
		})
	}
	return results
}

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 (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"
	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:         strings.ToUpper(strings.TrimSpace(announce.Network)),
		Operations:      cloneStrings(announce.Operations),
		Currencies:      cloneStrings(announce.Currencies),
		Limits:          cloneLimits(announce.Limits),
		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 = strings.ToUpper(strings.TrimSpace(entry.Rail))
	entry.Network = strings.ToUpper(strings.TrimSpace(entry.Network))
	entry.Operations = normalizeStrings(entry.Operations, false)
	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 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 = strings.ToUpper(strings.TrimSpace(announce.Rail))
	announce.Network = strings.ToUpper(strings.TrimSpace(announce.Network))
	announce.Operations = normalizeStrings(announce.Operations, false)
	announce.Currencies = normalizeStrings(announce.Currencies, true)
	announce.InvokeURI = strings.TrimSpace(announce.InvokeURI)
	announce.Version = strings.TrimSpace(announce.Version)
	announce.Health = normalizeHealth(announce.Health)
	if announce.Limits != nil {
		announce.Limits = normalizeLimits(*announce.Limits)
	}
	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 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
}
