package discovery

import (
	"context"
	"encoding/json"
	"strings"
	"sync"

	"github.com/google/uuid"
	msg "github.com/tech/sendico/pkg/messaging"
	mb "github.com/tech/sendico/pkg/messaging/broker"
	cons "github.com/tech/sendico/pkg/messaging/consumer"
	me "github.com/tech/sendico/pkg/messaging/envelope"
	msgproducer "github.com/tech/sendico/pkg/messaging/producer"
	"github.com/tech/sendico/pkg/merrors"
	"github.com/tech/sendico/pkg/mlogger"
	"go.uber.org/zap"
)

type Client struct {
	logger   mlogger.Logger
	producer msg.Producer
	consumer msg.Consumer
	sender   string

	mu      sync.Mutex
	pending map[string]chan LookupResponse
}

func NewClient(logger mlogger.Logger, msgBroker mb.Broker, producer msg.Producer, sender string) (*Client, error) {
	if msgBroker == nil {
		return nil, merrors.InvalidArgument("discovery client: broker is nil")
	}
	if logger == nil {
		logger = zap.NewNop()
	}
	logger = logger.Named("discovery_client")
	if producer == nil {
		producer = msgproducer.NewProducer(logger, msgBroker)
	}
	sender = strings.TrimSpace(sender)
	if sender == "" {
		sender = "discovery_client"
	}

	consumer, err := cons.NewConsumer(logger, msgBroker, LookupResponseEvent())
	if err != nil {
		return nil, err
	}

	client := &Client{
		logger:   logger,
		producer: producer,
		consumer: consumer,
		sender:   sender,
		pending:  map[string]chan LookupResponse{},
	}

	go func() {
		if err := consumer.ConsumeMessages(client.handleLookupResponse); err != nil {
			client.logger.Warn("Discovery lookup consumer stopped", zap.String("event", LookupResponseEvent().ToString()), zap.Error(err))
		}
	}()

	return client, nil
}

func (c *Client) Close() {
	if c == nil {
		return
	}
	if c.consumer != nil {
		c.consumer.Close()
	}
	c.mu.Lock()
	for key, ch := range c.pending {
		close(ch)
		delete(c.pending, key)
	}
	c.mu.Unlock()
}

func (c *Client) Lookup(ctx context.Context) (LookupResponse, error) {
	if c == nil || c.producer == nil {
		return LookupResponse{}, merrors.Internal("discovery client: producer not configured")
	}
	requestID := uuid.NewString()
	ch := make(chan LookupResponse, 1)

	c.mu.Lock()
	c.pending[requestID] = ch
	c.mu.Unlock()

	defer func() {
		c.mu.Lock()
		delete(c.pending, requestID)
		c.mu.Unlock()
	}()

	req := LookupRequest{RequestID: requestID}
	if err := c.producer.SendMessage(NewLookupRequestEnvelope(c.sender, req)); err != nil {
		return LookupResponse{}, err
	}

	select {
	case <-ctx.Done():
		return LookupResponse{}, ctx.Err()
	case resp := <-ch:
		return resp, nil
	}
}

func (c *Client) handleLookupResponse(_ context.Context, env me.Envelope) error {
	var payload LookupResponse
	if err := json.Unmarshal(env.GetData(), &payload); err != nil {
		fields := append(envelopeFields(env), zap.Int("data_len", len(env.GetData())), zap.Error(err))
		c.logWarn("Failed to decode discovery lookup response", fields...)
		return err
	}
	requestID := strings.TrimSpace(payload.RequestID)
	if requestID == "" {
		return nil
	}

	c.mu.Lock()
	ch := c.pending[requestID]
	c.mu.Unlock()
	if ch != nil {
		ch <- payload
	}
	return nil
}

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