package jobs

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"time"

	ckf "github.com/confluentinc/confluent-kafka-go/v2/kafka"
	"github.com/google/uuid"
	"github.com/rs/zerolog/log"
	"github.com/warmbly/warmbly/internal/app/advanced"
	"github.com/warmbly/warmbly/internal/config"
	"github.com/warmbly/warmbly/internal/events"
	"github.com/warmbly/warmbly/internal/infrastructure/kafka"
	"github.com/warmbly/warmbly/internal/infrastructure/pubsub"
	"github.com/warmbly/warmbly/internal/models"
	"github.com/warmbly/warmbly/internal/repository"
)

// TrackingConsumer handles tracking events from the Rust tracking service
type TrackingConsumer struct {
	consumer             *kafka.Consumer
	taskRepo             repository.TaskRepository
	campaignProgressRepo repository.CampaignProgressRepository
	campaignRepo         repository.CampaignRepository
	contactRepo          repository.ContactRepository
	streamingPublisher   *pubsub.StreamingPublisher
	dedupeRepo           repository.TrackingDedupeRepository
	// advancedService fires INSTANT open/click action chains the moment a
	// tracking event lands (the open/click analog of the reply path in
	// ProcessIncomingReply). Best-effort and nil-safe: when unset, opens/clicks
	// are still recorded and routed at the next step boundary by the scheduler.
	advancedService advanced.Service
	topic           string
}

// NewTrackingConsumer creates a new tracking consumer using existing Kafka infrastructure
// Config is loaded from AWS Parameter Store/Secrets Manager via config.LoadTrackingConsumerConfig
func NewTrackingConsumer(
	cfg *config.TrackingConsumerConfig,
	avrov2 *kafka.Avrov2,
	taskRepo repository.TaskRepository,
	campaignProgressRepo repository.CampaignProgressRepository,
	campaignRepo repository.CampaignRepository,
	contactRepo repository.ContactRepository,
	streamingPublisher *pubsub.StreamingPublisher,
	dedupeRepo repository.TrackingDedupeRepository,
	advancedService advanced.Service,
) (*TrackingConsumer, error) {
	// Create Kafka consumer using existing infrastructure
	consumerConfig := kafka.NewConsumer(cfg.Brokers)
	consumerConfig.Set("group.id", cfg.GroupID)
	consumerConfig.Set("auto.offset.reset", "earliest")
	consumerConfig.Set("enable.auto.commit", false)

	// Configure SASL if enabled (credentials from AWS Secrets Manager)
	if cfg.SASLEnabled {
		consumerConfig.WithSASL(&kafka.SASLConfig{
			Username: cfg.SASLUsername,
			Password: cfg.SASLPassword,
		})
	}

	consumer, err := consumerConfig.Connect()
	if err != nil {
		return nil, fmt.Errorf("failed to create tracking consumer: %w", err)
	}

	// Attach Avrov2 for deserialization
	consumer.WithAvrov2(avrov2)

	// Subscribe to tracking events topic
	if err := consumer.SubscribeTopics([]string{cfg.Topic}); err != nil {
		consumer.Close()
		return nil, fmt.Errorf("failed to subscribe to tracking topic: %w", err)
	}

	return &TrackingConsumer{
		consumer:             consumer,
		taskRepo:             taskRepo,
		campaignProgressRepo: campaignProgressRepo,
		campaignRepo:         campaignRepo,
		contactRepo:          contactRepo,
		streamingPublisher:   streamingPublisher,
		dedupeRepo:           dedupeRepo,
		advancedService:      advancedService,
		topic:                cfg.Topic,
	}, nil
}

// Start begins consuming tracking events
func (tc *TrackingConsumer) Start(ctx context.Context) error {
	return tc.consumer.Consume(ctx, tc.handleMessage)
}

// Close closes the consumer
func (tc *TrackingConsumer) Close() {
	if tc.consumer != nil {
		tc.consumer.Close()
	}
}

// handleMessage processes a raw Kafka message using Avro deserialization
func (tc *TrackingConsumer) handleMessage(msg *ckf.Message) error {
	var event events.TrackingEvent

	// Deserialize using Confluent Avrov2
	if err := tc.consumer.Avrov2.Deser.DeserializeInto(tc.topic, msg.Value, &event); err != nil {
		log.Warn().Err(err).Msg("failed to deserialize tracking event")
		return nil // Don't fail - skip invalid events
	}

	return tc.HandleTrackingEvent(context.Background(), &event)
}

// HandleTrackingEvent processes a tracking event
func (tc *TrackingConsumer) HandleTrackingEvent(ctx context.Context, event *events.TrackingEvent) error {
	// Parse and validate task ID
	taskID, err := uuid.Parse(event.TaskID)
	if err != nil {
		// Invalid task ID, skip
		return nil
	}

	// Calculate URL hash for click event deduplication
	urlHash := ""
	if event.EventType == events.EventTypeEmailClicked && event.OriginalURL != nil && *event.OriginalURL != "" {
		urlHash = hashURL(*event.OriginalURL)
	}

	// Classify opens: machine fetches (Apple MPP prefetch, UA-less clients)
	// still count as delivery signal but are labeled, and must never fire
	// open-triggered automations (a prefetch is not intent).
	machineOpen := event.EventType == events.EventTypeEmailOpened && isMachineOpen(event.UserAgent)

	// Check for duplicate at consumer level (belt and suspenders with Rust service)
	if tc.dedupeRepo != nil {
		processed, err := tc.dedupeRepo.IsProcessed(ctx, taskID, event.EventType, urlHash)
		if err != nil {
			// Log but continue - allow processing on dedupe errors
			log.Warn().Err(err).Str("task_id", event.TaskID).Msg("tracking dedupe check failed")
		} else if processed {
			// A HUMAN open after a machine-labeled one upgrades the label
			// (MPP prefetched at delivery; the person actually read it later
			// from another network). Quiet write only: the open was already
			// counted once, so no automations and no re-publish.
			if event.EventType == events.EventTypeEmailOpened && !machineOpen {
				if campaignTask, terr := tc.taskRepo.GetCampaignTask(ctx, taskID); terr == nil &&
					campaignTask != nil && campaignTask.CampaignID != nil &&
					campaignTask.ContactID != nil && campaignTask.SequenceID != nil {
					_ = tc.campaignProgressRepo.RecordEmailOpened(ctx,
						*campaignTask.CampaignID, *campaignTask.ContactID, *campaignTask.SequenceID, false)
				}
			}
			return nil
		}
	}

	// Get campaign task to find campaign/contact/sequence IDs
	campaignTask, err := tc.taskRepo.GetCampaignTask(ctx, taskID)
	if err != nil {
		log.Warn().Err(err).Str("task_id", event.TaskID).Msg("failed to get campaign task for tracking event")
		return nil
	}

	if campaignTask == nil || campaignTask.CampaignID == nil {
		// Task not found or not a campaign task, skip
		return nil
	}

	// Ensure we have contact_id and sequence_id
	if campaignTask.ContactID == nil || campaignTask.SequenceID == nil {
		// Missing required fields, skip
		return nil
	}

	// Record the event, then fire any INSTANT open/click action chain for the
	// contact's current step the moment the signal lands (the open/click analog of
	// the reply path in ProcessIncomingReply). instantKind maps the tracking event
	// to the matcher's eventKind. Firing happens AFTER the Record* write so the
	// matcher reads the just-stamped opened_at / clicked_at off the progress row.
	var instantKind string
	switch event.EventType {
	case events.EventTypeEmailOpened:
		err = tc.campaignProgressRepo.RecordEmailOpened(ctx,
			*campaignTask.CampaignID,
			*campaignTask.ContactID,
			*campaignTask.SequenceID,
			machineOpen)
		if !machineOpen {
			instantKind = "open"
		}
	case events.EventTypeEmailClicked:
		err = tc.campaignProgressRepo.RecordEmailClicked(ctx,
			*campaignTask.CampaignID,
			*campaignTask.ContactID,
			*campaignTask.SequenceID)
		instantKind = "click"
	default:
		// Unknown event type, skip
		return nil
	}

	if err != nil {
		log.Error().Err(err).Str("task_id", event.TaskID).Str("event_type", string(event.EventType)).Msg("failed to record tracking event")
		return nil
	}

	// INSTANT open/click trigger: best-effort and non-blocking, mirroring the
	// reply path. A failure (or a nil service in a process that doesn't wire it)
	// must never block tracking ingest; the scheduler still routes the matching
	// opened/clicked branch at the next step boundary. Exactly-once per (step,
	// eventKind) is enforced inside FireInstantActions via ClaimInstantFire.
	if tc.advancedService != nil && instantKind != "" {
		tc.advancedService.FireInstantActions(ctx,
			*campaignTask.CampaignID,
			*campaignTask.ContactID,
			*campaignTask.SequenceID,
			instantKind)
	}

	// Mark as processed for deduplication
	if tc.dedupeRepo != nil {
		if err := tc.dedupeRepo.MarkProcessed(ctx, taskID, event.EventType, urlHash); err != nil {
			log.Warn().Err(err).Str("task_id", event.TaskID).Msg("failed to mark tracking event as processed")
		}
	}

	// Publish to Pub/Sub for realtime updates
	tc.publishTrackingEvent(ctx, campaignTask, *event, machineOpen)

	return nil
}

// publishTrackingEvent publishes the tracking event to Pub/Sub for realtime UI
// updates AND fans an opt-in firehose webhook (campaign.email_opened/clicked).
func (tc *TrackingConsumer) publishTrackingEvent(ctx context.Context, task *repository.CampaignTask, event events.TrackingEvent, machine bool) {
	// Get campaign to find user ID + org
	campaign, err := tc.campaignRepo.GetByID(ctx, *task.CampaignID)
	if err != nil || campaign == nil {
		return
	}

	// Get contact email for display
	var contactEmail string
	if task.ContactID != nil {
		contact, xerr := tc.contactRepo.GetByID(ctx, *task.ContactID)
		if xerr == nil && contact != nil {
			contactEmail = contact.Email
		}
	}

	// Fan an opt-in firehose webhook for the open/click (org-scoped). Human opens
	// only — a machine prefetch is not engagement intent — clicks always.
	if tc.advancedService != nil && campaign.OrganizationID != nil {
		var whType models.WebhookEventType
		switch {
		case event.EventType == events.EventTypeEmailOpened && !machine:
			whType = models.WebhookEventCampaignEmailOpened
		case event.EventType == events.EventTypeEmailClicked:
			whType = models.WebhookEventCampaignEmailClicked
		}
		if whType != "" {
			data := map[string]any{
				"campaign_id":   task.CampaignID.String(),
				"contact_id":    task.ContactID.String(),
				"contact_email": contactEmail,
				"sequence_id":   task.SequenceID.String(),
			}
			if event.EventType == events.EventTypeEmailClicked && event.OriginalURL != nil {
				data["url"] = *event.OriginalURL
			}
			tc.advancedService.EmitCampaignEvent(ctx, *campaign.OrganizationID, whType, data)
		}
	}

	if tc.streamingPublisher == nil {
		return
	}

	// Determine event type
	var eventType pubsub.EventType
	switch event.EventType {
	case events.EventTypeEmailOpened:
		eventType = pubsub.EventEmailOpened
	case events.EventTypeEmailClicked:
		eventType = pubsub.EventEmailClicked
	default:
		return
	}

	// Publish tracking event (org-scoped: opens/clicks pulse live for the
	// whole team, not just the campaign owner)
	var orgID string
	if campaign.OrganizationID != nil {
		orgID = campaign.OrganizationID.String()
	}
	trackingPayload := &pubsub.TrackingEventPayload{
		BaseEvent: pubsub.BaseEvent{
			EventType: eventType,
			UserID:    campaign.UserID,
			Timestamp: time.Now(),
		},
		OrgID:        orgID,
		CampaignID:   task.CampaignID.String(),
		ContactID:    task.ContactID.String(),
		ContactEmail: contactEmail,
		SequenceID:   task.SequenceID.String(),
		Machine:      machine,
	}

	if event.EventType == events.EventTypeEmailClicked && event.OriginalURL != nil {
		trackingPayload.OriginalURL = *event.OriginalURL
	}

	tc.streamingPublisher.PublishTrackingEvent(ctx, trackingPayload)
}

// hashURL creates a short hash of a URL for deduplication
func hashURL(u string) string {
	if u == "" {
		return ""
	}
	h := sha256.Sum256([]byte(u))
	return hex.EncodeToString(h[:8])
}
