package pubsub

import (
	"context"
	"encoding/json"
	"fmt"
	"time"

	"cloud.google.com/go/pubsub"
	"github.com/getsentry/sentry-go"
	"github.com/google/uuid"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

// Client wraps Google Pub/Sub for real-time streaming
type Client struct {
	client    *pubsub.Client
	projectID string
}

// NewClient creates a new Pub/Sub client
func NewClient(ctx context.Context, projectID string) (*Client, error) {
	client, err := pubsub.NewClient(ctx, projectID)
	if err != nil {
		return nil, fmt.Errorf("failed to create pubsub client: %w", err)
	}

	return &Client{
		client:    client,
		projectID: projectID,
	}, nil
}

// Close closes the Pub/Sub client
func (c *Client) Close() error {
	return c.client.Close()
}

// getTopic gets or creates a topic
func (c *Client) getTopic(ctx context.Context, topicID string) (*pubsub.Topic, error) {
	topic := c.client.Topic(topicID)
	exists, err := topic.Exists(ctx)
	if err != nil {
		return nil, err
	}

	if !exists {
		topic, err = c.client.CreateTopic(ctx, topicID)
		if err != nil {
			return nil, err
		}
	}

	return topic, nil
}

// realtimeTopology is the full topic -> pull-subscription set the Elixir
// realtime service consumes. It is the single source of truth for provisioning:
// every topic the StreamingPublisher writes to gets exactly one subscription
// named "<topic>-sub", matching :realtime, :pubsub_subscriptions in the Elixir
// config (realtime/config/{config,runtime}.exs). Keep the two lists in lockstep.
var realtimeTopology = map[string]string{
	TopicTaskStatus:     TopicTaskStatus + "-sub",
	TopicCampaignUpdate: TopicCampaignUpdate + "-sub",
	TopicWarmupUpdate:   TopicWarmupUpdate + "-sub",
	TopicEmailError:     TopicEmailError + "-sub",
	TopicEmailWarning:   TopicEmailWarning + "-sub",
	TopicUserEvents:     TopicUserEvents + "-sub",
	TopicEmailInbox:     TopicEmailInbox + "-sub",
	TopicBulkOps:        TopicBulkOps + "-sub",
	TopicContactsSync:   TopicContactsSync + "-sub",
}

// EnsureRealtimeTopology idempotently creates every realtime topic and its pull
// subscription. Safe to call on every boot and from multiple services
// concurrently: AlreadyExists is treated as success. Provisioning here (the
// control plane) means the Elixir Broadway producers always find their
// subscriptions, instead of silently consuming from a subscription that was
// never created. Called only when PUBSUB_ENABLED=true.
func (c *Client) EnsureRealtimeTopology(ctx context.Context) error {
	for topicID, subID := range realtimeTopology {
		topic, err := c.client.CreateTopic(ctx, topicID)
		if err != nil {
			if status.Code(err) != codes.AlreadyExists {
				return fmt.Errorf("create topic %s: %w", topicID, err)
			}
			topic = c.client.Topic(topicID)
		}
		if _, err := c.client.CreateSubscription(ctx, subID, pubsub.SubscriptionConfig{
			Topic:       topic,
			AckDeadline: 30 * time.Second,
		}); err != nil && status.Code(err) != codes.AlreadyExists {
			return fmt.Errorf("create subscription %s: %w", subID, err)
		}
	}
	return nil
}

// Publish publishes a message to a topic
func (c *Client) Publish(ctx context.Context, topicID string, data interface{}, attributes map[string]string) error {
	topic, err := c.getTopic(ctx, topicID)
	if err != nil {
		return fmt.Errorf("failed to get topic: %w", err)
	}

	jsonData, err := json.Marshal(data)
	if err != nil {
		return fmt.Errorf("failed to marshal data: %w", err)
	}

	result := topic.Publish(ctx, &pubsub.Message{
		Data:       jsonData,
		Attributes: attributes,
	})

	_, err = result.Get(ctx)
	if err != nil {
		return fmt.Errorf("failed to publish message: %w", err)
	}

	return nil
}

// eventBus is the transport the StreamingPublisher writes to. Both the Google
// Pub/Sub Client and the Redis bridge implement it, so the publish helpers stay
// transport-agnostic and the same code path serves GCP and non-GCP (local dev)
// environments.
type eventBus interface {
	Publish(ctx context.Context, topicID string, data interface{}, attributes map[string]string) error
}

// StreamingPublisher handles real-time streaming to users
type StreamingPublisher struct {
	client eventBus
}

// NewStreamingPublisher creates a new streaming publisher
func NewStreamingPublisher(client eventBus) *StreamingPublisher {
	return &StreamingPublisher{
		client: client,
	}
}

// Topic names for real-time updates
const (
	TopicTaskStatus     = "task-status"
	TopicCampaignUpdate = "campaign-update"
	TopicWarmupUpdate   = "warmup-update"
	TopicEmailError     = "email-error"
	TopicEmailWarning   = "email-warning"
)

// Event types
type EventType string

const (
	EventTaskCreated   EventType = "TASK_CREATED"
	EventTaskStarted   EventType = "TASK_STARTED"
	EventTaskCompleted EventType = "TASK_COMPLETED"
	EventTaskFailed    EventType = "TASK_FAILED"
	EventEmailSent     EventType = "EMAIL_SENT"
	EventEmailFailed   EventType = "EMAIL_FAILED"
	EventError         EventType = "ERROR"
	EventWarning       EventType = "WARNING"
)

// StreamEvent represents a real-time event for users
type StreamEvent struct {
	EventType EventType `json:"event_type"`
	UserID    string    `json:"user_id"`
	TaskID    string    `json:"task_id,omitempty"`
	EmailID   string    `json:"email_id,omitempty"`
	Message   string    `json:"message,omitempty"`
	Data      any       `json:"data,omitempty"`
	Timestamp time.Time `json:"timestamp"`
}

// PublishTaskStatus publishes a task status update
func (p *StreamingPublisher) PublishTaskStatus(ctx context.Context, userID string, taskID uuid.UUID, eventType EventType, message string, data any) {
	if p.client == nil {
		return
	}

	event := StreamEvent{
		EventType: eventType,
		UserID:    userID,
		TaskID:    taskID.String(),
		Message:   message,
		Data:      data,
		Timestamp: time.Now(),
	}

	attrs := map[string]string{
		"user_id":    userID,
		"event_type": string(eventType),
	}

	if err := p.client.Publish(ctx, TopicTaskStatus, event, attrs); err != nil {
		sentry.CaptureException(fmt.Errorf("failed to publish task status: %w", err))
	}
}

// PublishEmailError publishes an email error for user visibility
func (p *StreamingPublisher) PublishEmailError(ctx context.Context, userID string, emailID uuid.UUID, taskID uuid.UUID, errorTitle, errorMessage string) {
	if p.client == nil {
		return
	}

	event := StreamEvent{
		EventType: EventError,
		UserID:    userID,
		TaskID:    taskID.String(),
		EmailID:   emailID.String(),
		Message:   errorTitle,
		Data: map[string]string{
			"title":   errorTitle,
			"message": errorMessage,
		},
		Timestamp: time.Now(),
	}

	attrs := map[string]string{
		"user_id":    userID,
		"email_id":   emailID.String(),
		"event_type": string(EventError),
	}

	if err := p.client.Publish(ctx, TopicEmailError, event, attrs); err != nil {
		sentry.CaptureException(fmt.Errorf("failed to publish email error: %w", err))
	}
}

// PublishEmailWarning publishes an email warning for user visibility
func (p *StreamingPublisher) PublishEmailWarning(ctx context.Context, userID string, emailID uuid.UUID, warningTitle, warningMessage string) {
	if p.client == nil {
		return
	}

	event := StreamEvent{
		EventType: EventWarning,
		UserID:    userID,
		EmailID:   emailID.String(),
		Message:   warningTitle,
		Data: map[string]string{
			"title":   warningTitle,
			"message": warningMessage,
		},
		Timestamp: time.Now(),
	}

	attrs := map[string]string{
		"user_id":    userID,
		"email_id":   emailID.String(),
		"event_type": string(EventWarning),
	}

	if err := p.client.Publish(ctx, TopicEmailWarning, event, attrs); err != nil {
		sentry.CaptureException(fmt.Errorf("failed to publish email warning: %w", err))
	}
}

// PublishCampaignProgress publishes campaign progress update
func (p *StreamingPublisher) PublishCampaignProgress(ctx context.Context, userID string, campaignID uuid.UUID, progress any) {
	if p.client == nil {
		return
	}

	event := StreamEvent{
		EventType: EventTaskCompleted,
		UserID:    userID,
		Message:   "Campaign progress updated",
		Data:      progress,
		Timestamp: time.Now(),
	}

	attrs := map[string]string{
		"user_id":     userID,
		"campaign_id": campaignID.String(),
		"event_type":  "CAMPAIGN_PROGRESS",
	}

	if err := p.client.Publish(ctx, TopicCampaignUpdate, event, attrs); err != nil {
		sentry.CaptureException(fmt.Errorf("failed to publish campaign progress: %w", err))
	}
}

// PublishWarmupStats publishes warmup statistics update
func (p *StreamingPublisher) PublishWarmupStats(ctx context.Context, userID string, emailID uuid.UUID, stats any) {
	if p.client == nil {
		return
	}

	event := StreamEvent{
		EventType: EventTaskCompleted,
		UserID:    userID,
		EmailID:   emailID.String(),
		Message:   "Warmup statistics updated",
		Data:      stats,
		Timestamp: time.Now(),
	}

	attrs := map[string]string{
		"user_id":    userID,
		"email_id":   emailID.String(),
		"event_type": "WARMUP_STATS",
	}

	if err := p.client.Publish(ctx, TopicWarmupUpdate, event, attrs); err != nil {
		sentry.CaptureException(fmt.Errorf("failed to publish warmup stats: %w", err))
	}
}
