package discovery

import (
	"os"
	"strings"
	"sync"
	"time"

	"github.com/google/uuid"
	msg "github.com/tech/sendico/pkg/messaging"
	"github.com/tech/sendico/pkg/mlogger"
	"go.uber.org/zap"
)

type Announcer struct {
	logger   mlogger.Logger
	producer msg.Producer
	sender   string
	announce Announcement

	startOnce sync.Once
	stopOnce  sync.Once
	stopCh    chan struct{}
	doneCh    chan struct{}
}

func NewAnnouncer(logger mlogger.Logger, producer msg.Producer, sender string, announce Announcement) *Announcer {
	if logger == nil {
		logger = zap.NewNop()
	}
	logger = logger.Named("discovery")
	announce = normalizeAnnouncement(announce)
	if announce.Service == "" {
		announce.Service = strings.TrimSpace(sender)
	}
	if announce.InstanceID == "" {
		announce.InstanceID = InstanceID()
	}
	if announce.ID == "" {
		announce.ID = DefaultEntryID(announce.Service)
	}
	return &Announcer{
		logger:   logger,
		producer: producer,
		sender:   strings.TrimSpace(sender),
		announce: announce,
		stopCh:   make(chan struct{}),
		doneCh:   make(chan struct{}),
	}
}

func (a *Announcer) Start() {
	if a == nil {
		return
	}
	a.startOnce.Do(func() {
		if a.producer == nil {
			a.logWarn("Discovery announce skipped: producer not configured", announcementFields(a.announce)...)
			close(a.doneCh)
			return
		}
		if strings.TrimSpace(a.announce.ID) == "" {
			a.logWarn("Discovery announce skipped: missing instance id", announcementFields(a.announce)...)
			close(a.doneCh)
			return
		}
		a.logInfo("Discovery announcer starting", announcementFields(a.announce)...)
		a.sendAnnouncement()
		a.sendHeartbeat()
		go a.heartbeatLoop()
	})
}

func (a *Announcer) Stop() {
	if a == nil {
		return
	}
	a.stopOnce.Do(func() {
		close(a.stopCh)
		<-a.doneCh
		a.logInfo("Discovery announcer stopped", announcementFields(a.announce)...)
	})
}

func (a *Announcer) heartbeatLoop() {
	defer close(a.doneCh)
	interval := time.Duration(a.announce.Health.IntervalSec) * time.Second
	if interval <= 0 {
		interval = time.Duration(DefaultHealthIntervalSec) * time.Second
	}
	ticker := time.NewTicker(interval)
	defer ticker.Stop()

	for {
		select {
		case <-a.stopCh:
			return
		case <-ticker.C:
			a.sendHeartbeat()
		}
	}
}

func (a *Announcer) sendAnnouncement() {
	env := NewServiceAnnounceEnvelope(a.sender, a.announce)
	event := ServiceAnnounceEvent()
	if a.announce.Rail != "" {
		env = NewGatewayAnnounceEnvelope(a.sender, a.announce)
		event = GatewayAnnounceEvent()
	}
	if err := a.producer.SendMessage(env); err != nil {
		fields := append(announcementFields(a.announce), zap.String("event", event.ToString()), zap.Error(err))
		a.logWarn("Failed to publish discovery announce", fields...)
		return
	}
	a.logInfo("Discovery announce published", append(announcementFields(a.announce), zap.String("event", event.ToString()))...)
}

func (a *Announcer) sendHeartbeat() {
	hb := Heartbeat{
		ID:         a.announce.ID,
		InstanceID: a.announce.InstanceID,
		Status:     "ok",
		TS:         time.Now().Unix(),
	}
	if err := a.producer.SendMessage(NewHeartbeatEnvelope(a.sender, hb)); err != nil {
		fields := append(announcementFields(a.announce), zap.String("event", HeartbeatEvent().ToString()), zap.Error(err))
		a.logWarn("Failed to publish discovery heartbeat", fields...)
	}
}

func (a *Announcer) logInfo(message string, fields ...zap.Field) {
	if a == nil {
		return
	}
	a.logger.Info(message, fields...)
}

func (a *Announcer) logWarn(message string, fields ...zap.Field) {
	if a == nil {
		return
	}
	a.logger.Warn(message, fields...)
}

func DefaultEntryID(service string) string {
	clean := strings.ToLower(strings.TrimSpace(service))
	if clean == "" {
		clean = "service"
	}
	host, _ := os.Hostname()
	host = strings.ToLower(strings.TrimSpace(host))
	uid := uuid.NewString()
	if host == "" {
		return clean + "_" + uid
	}
	return clean + "_" + host + "_" + uid
}

func DefaultInstanceID(service string) string {
	return DefaultEntryID(service)
}

func DefaultInvokeURI(service string) string {
	clean := strings.ToLower(strings.TrimSpace(service))
	if clean == "" {
		return ""
	}
	return clean
}
