package rabbitmq

import (
	"context"
	"fmt"

	"github.com/charmingruby/homunc/apps/fraud/pkg/telemetry"
	"github.com/streadway/amqp"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/propagation"
	"go.opentelemetry.io/otel/trace"
)

type Client struct {
	conn    *amqp.Connection
	channel *amqp.Channel
	log     *telemetry.Logger
	cfg     *Config
}

type Config struct {
	URL       string
	QueueName string
}

func New(log *telemetry.Logger, cfg *Config) (*Client, error) {
	conn, err := amqp.Dial(cfg.URL)
	if err != nil {
		return nil, fmt.Errorf("failed to connect to RabbitMQ: %w", err)
	}

	ch, err := conn.Channel()
	if err != nil {
		conn.Close()
		return nil, fmt.Errorf("failed to create channel: %w", err)
	}

	if _, err := ch.QueueDeclare(cfg.QueueName, true, false, false, false, nil); err != nil {
		conn.Close()
		return nil, fmt.Errorf("failed to declare queue: %w", err)
	}

	return &Client{
		conn:    conn,
		channel: ch,
		log:     log,
		cfg:     cfg,
	}, nil
}

func (c *Client) Close() {
	if c.channel != nil {
		c.channel.Close()
	}

	if c.conn != nil {
		c.conn.Close()
	}
}

type PublishInput struct {
	Exchange   string
	RoutingKey string
	Message    []byte
}

func (c *Client) Publish(ctx context.Context, in PublishInput) (context.Context, error) {
	err := c.channel.ExchangeDeclare(
		in.Exchange, // name
		"topic",     // type
		true,        // durable
		false,       // auto-deleted
		false,       // internal
		false,       // no-wait
		nil,         // arguments
	)
	if err != nil {
		return ctx, fmt.Errorf("failed to declare exchange: %w", err)
	}

	err = c.channel.Publish(
		in.Exchange,
		in.RoutingKey,
		false,
		false,
		amqp.Publishing{
			ContentType: "application/json",
			Body:        in.Message,
		},
	)
	if err != nil {
		return ctx, fmt.Errorf("failed to publish message: %w", err)
	}

	return ctx, nil
}

func (c *Client) Subscribe(ctx context.Context, exchange, routingKey string, handler func(context.Context, []byte) error) error {
	if err := c.channel.ExchangeDeclare(
		exchange,
		"topic",
		true,
		false,
		false,
		false,
		nil,
	); err != nil {
		return fmt.Errorf("failed to declare exchange: %w", err)
	}

	if _, err := c.channel.QueueDeclare(
		c.cfg.QueueName,
		true,
		false,
		false,
		false,
		nil,
	); err != nil {
		return fmt.Errorf("failed to declare queue: %w", err)
	}

	if err := c.channel.QueueBind(
		c.cfg.QueueName,
		routingKey,
		exchange,
		false,
		nil,
	); err != nil {
		return fmt.Errorf("failed to bind queue: %w", err)
	}

	msgs, err := c.channel.Consume(
		c.cfg.QueueName,
		"",
		false,
		false,
		false,
		false,
		nil,
	)
	if err != nil {
		return fmt.Errorf("failed to subscribe: %w", err)
	}

	go func() {
		propagator := propagation.TraceContext{}
		tracer := otel.Tracer("fraud.rabbitmq.subscriber")

		for msg := range msgs {
			carrier := headerCarrier(msg.Headers)
			ctxExtractedFromMsgHeaders := propagator.Extract(ctx, carrier)

			remoteSpanCtx := trace.SpanContextFromContext(ctxExtractedFromMsgHeaders)
			if !remoteSpanCtx.IsValid() {
				c.log.Warn("invalid or missing remote span context in headers")
			}

			parentCtx := trace.ContextWithRemoteSpanContext(ctx, remoteSpanCtx)

			ctxWithSpan, span := tracer.Start(parentCtx, "ConsumeMessage")

			if err := handler(ctxWithSpan, msg.Body); err != nil {
				c.log.Error("failed to handle message", "error", err, "message_id", msg.MessageId)

				if err := msg.Nack(false, true); err != nil {
					c.log.Error("failed to nack message", "error", err, "message_id", msg.MessageId)
				}

				span.RecordError(err)
				span.End()

				continue
			}

			if err := msg.Ack(false); err != nil {
				c.log.Error("failed to ack message", "error", err, "message_id", msg.MessageId)

				span.RecordError(err)
			}

			span.End()
		}
	}()

	return nil
}
