package app

import (
	"context"
	"sync"
	"sync/atomic"
	"time"

	"github.com/antitbone/ja4/correlator/internal/domain"
	"github.com/antitbone/ja4/correlator/internal/ports"
)

const (
	// DefaultEventChannelBufferSize is the default size for event channels
	DefaultEventChannelBufferSize = 1000
	// OrphanTickInterval is how often the orchestrator drains pending orphans.
	// Set to half the default emit delay (500ms/2) so orphans are emitted promptly
	// even when no new events arrive.
	OrphanTickInterval = 250 * time.Millisecond
)

// OrchestratorConfig holds the orchestrator configuration.
type OrchestratorConfig struct {
	Sources []ports.EventSource
	Sink    ports.CorrelatedLogSink
}

// Orchestrator connects sources to the correlation service and sinks.
type Orchestrator struct {
	config           OrchestratorConfig
	correlationSvc   ports.CorrelationProcessor
	ctx              context.Context
	cancel           context.CancelFunc
	wg               sync.WaitGroup
	running          atomic.Bool
}

// NewOrchestrator creates a new orchestrator.
func NewOrchestrator(config OrchestratorConfig, correlationSvc ports.CorrelationProcessor) *Orchestrator {
	ctx, cancel := context.WithCancel(context.Background())
	return &Orchestrator{
		config:         config,
		correlationSvc: correlationSvc,
		ctx:            ctx,
		cancel:         cancel,
	}
}

// Start begins the orchestration.
func (o *Orchestrator) Start() error {
	if !o.running.CompareAndSwap(false, true) {
		return nil // Already running
	}

	// Start each source
	for _, source := range o.config.Sources {
		eventChan := make(chan *domain.NormalizedEvent, DefaultEventChannelBufferSize)

		o.wg.Add(1)
		go func(src ports.EventSource, evChan chan *domain.NormalizedEvent) {
			defer o.wg.Done()

			// Start the source in a separate goroutine
			sourceErr := make(chan error, 1)
			go func() {
				if err := src.Start(o.ctx, evChan); err != nil {
					sourceErr <- err
				}
			}()

			// Process events in the current goroutine
			o.processEvents(evChan)

			// Check for source start errors
			if err := <-sourceErr; err != nil {
				// Source failed to start, log error and exit
				return
			}
		}(source, eventChan)
	}

	// Start a periodic ticker to drain pending orphan A events independently of the
	// event flow. Without this, orphans are only emitted when a new event arrives,
	// causing them to accumulate silently when the source goes quiet.
	o.wg.Add(1)
	go func() {
		defer o.wg.Done()
		ticker := time.NewTicker(OrphanTickInterval)
		defer ticker.Stop()
		for {
			select {
			case <-o.ctx.Done():
				return
			case <-ticker.C:
				logs := o.correlationSvc.EmitPendingOrphans()
				for _, log := range logs {
					o.config.Sink.Write(o.ctx, log) //nolint:errcheck
				}
			}
		}
	}()

	return nil
}

// processEvents lit les événements du canal, les soumet au service de corrélation et écrit les résultats dans le puits.
func (o *Orchestrator) processEvents(eventChan <-chan *domain.NormalizedEvent) {
	for {
		select {
		case <-o.ctx.Done():
			// Drain remaining events before exiting
			for {
				select {
				case event, ok := <-eventChan:
					if !ok {
						return
					}
					logs := o.correlationSvc.ProcessEvent(event)
					for _, log := range logs {
						o.config.Sink.Write(o.ctx, log)
					}
				default:
					return
				}
			}
		case event, ok := <-eventChan:
			if !ok {
				return
			}

			// Process through correlation service
			logs := o.correlationSvc.ProcessEvent(event)

			// Write correlated logs to sink
			for _, log := range logs {
				if err := o.config.Sink.Write(o.ctx, log); err != nil {
					// Log error but continue processing
				}
			}
		}
	}
}

// Stop gracefully stops the orchestrator.
// It stops all sources and closes sinks immediately without waiting for queue drainage.
// systemd TimeoutStopSec handles forced termination if needed.
func (o *Orchestrator) Stop() error {
	if !o.running.CompareAndSwap(true, false) {
		return nil // Not running
	}

	// Cancel context to stop accepting new events immediately
	o.cancel()

	// Close sink (flush skipped - in-flight events are dropped)
	if err := o.config.Sink.Close(); err != nil {
		// Log error
	}

	return nil
}
