package loop

import (
	"context"
	"fmt"
	"log/slog"
	"sort"
	"strings"
	"sync"
	"time"
)

const detachedCompletionTimeout = 10 * time.Second

// Registry tracks all active loops and provides visibility into what is
// running. It enforces concurrency limits and coordinates graceful
// shutdown.
type Registry struct {
	mu       sync.RWMutex
	loops    map[string]*Loop
	maxLoops int
	logger   *slog.Logger
}

// RegistryOption configures a Registry.
type RegistryOption func(*Registry)

// WithMaxLoops sets the maximum number of concurrent loops the registry
// will allow. Zero means unlimited.
func WithMaxLoops(n int) RegistryOption {
	return func(r *Registry) {
		r.maxLoops = n
	}
}

// WithRegistryLogger sets the logger for registry operations. Nil is
// ignored (keeps slog.Default()).
func WithRegistryLogger(l *slog.Logger) RegistryOption {
	return func(r *Registry) {
		if l != nil {
			r.logger = l
		}
	}
}

// NewRegistry creates a new loop registry.
func NewRegistry(opts ...RegistryOption) *Registry {
	r := &Registry{
		loops:  make(map[string]*Loop),
		logger: slog.Default(),
	}
	for _, opt := range opts {
		opt(r)
	}
	return r
}

// Register adds a loop to the registry. Returns an error if the loop's
// ID is already registered or the concurrency limit would be exceeded.
// The loop is not started — call [Loop.Start] after registering.
func (r *Registry) Register(l *Loop) error {
	r.mu.Lock()
	defer r.mu.Unlock()

	if _, exists := r.loops[l.id]; exists {
		return fmt.Errorf("loop ID %q already registered", l.id)
	}
	if r.maxLoops > 0 && len(r.loops) >= r.maxLoops {
		return fmt.Errorf("concurrency limit reached (%d loops)", r.maxLoops)
	}

	r.loops[l.id] = l
	r.logger.Debug("loop registered",
		"loop_id", l.id,
		"loop_name", l.config.Name,
		"parent_id", l.config.ParentID,
		"active_loops", len(r.loops),
	)
	return nil
}

// Deregister removes a loop from the registry. Safe to call for a loop
// that is not registered (no-op).
func (r *Registry) Deregister(id string) {
	r.mu.Lock()
	defer r.mu.Unlock()

	if _, exists := r.loops[id]; !exists {
		return
	}
	delete(r.loops, id)
	r.logger.Debug("loop deregistered",
		"loop_id", id,
		"active_loops", len(r.loops),
	)
}

// Get returns the loop with the given ID, or nil if not found.
func (r *Registry) Get(id string) *Loop {
	r.mu.RLock()
	defer r.mu.RUnlock()
	return r.loops[id]
}

// GetByName returns the first loop with the given name, or nil if not
// found. If multiple loops share a name, the result is undefined.
func (r *Registry) GetByName(name string) *Loop {
	r.mu.RLock()
	defer r.mu.RUnlock()
	for _, l := range r.loops {
		if l.config.Name == name {
			return l
		}
	}
	return nil
}

// List returns a snapshot of all registered loops sorted by name.
func (r *Registry) List() []*Loop {
	r.mu.RLock()
	defer r.mu.RUnlock()

	result := make([]*Loop, 0, len(r.loops))
	for _, l := range r.loops {
		result = append(result, l)
	}
	sort.Slice(result, func(i, j int) bool {
		return result[i].config.Name < result[j].config.Name
	})
	return result
}

// Statuses returns a snapshot of all registered loop statuses sorted by
// name.
func (r *Registry) Statuses() []Status {
	r.mu.RLock()
	defer r.mu.RUnlock()

	result := make([]Status, 0, len(r.loops))
	for _, l := range r.loops {
		result = append(result, l.Status())
	}
	sort.Slice(result, func(i, j int) bool {
		return result[i].Name < result[j].Name
	})
	return result
}

// ActiveCount returns the number of registered loops.
func (r *Registry) ActiveCount() int {
	r.mu.RLock()
	defer r.mu.RUnlock()
	return len(r.loops)
}

// MaxLoops returns the configured concurrency limit. Zero means
// unlimited.
func (r *Registry) MaxLoops() int {
	r.mu.RLock()
	defer r.mu.RUnlock()
	return r.maxLoops
}

// FindByName returns all live loops with the exact given name.
func (r *Registry) FindByName(name string) []*Loop {
	r.mu.RLock()
	defer r.mu.RUnlock()

	var matches []*Loop
	for _, l := range r.loops {
		if l.config.Name == name {
			matches = append(matches, l)
		}
	}
	sort.Slice(matches, func(i, j int) bool {
		return matches[i].id < matches[j].id
	})
	return matches
}

// StopLoopByName cancels one registered loop by exact name. It returns
// an error when no loop matches or when the name is ambiguous.
func (r *Registry) StopLoopByName(name string) error {
	matches := r.FindByName(name)
	switch len(matches) {
	case 0:
		return fmt.Errorf("loop named %q not found", name)
	case 1:
		return r.StopLoop(matches[0].id)
	default:
		ids := make([]string, 0, len(matches))
		for _, l := range matches {
			ids = append(ids, l.id)
		}
		return fmt.Errorf("loop name %q is ambiguous; retry with loop_id from %v", name, ids)
	}
}

// ShutdownAll cancels all registered loops and waits for them to drain.
// The provided context controls the maximum time to wait; if it expires,
// remaining loops are abandoned. Returns the number of loops that were
// stopped.
func (r *Registry) ShutdownAll(ctx context.Context) int {
	r.mu.RLock()
	loops := make([]*Loop, 0, len(r.loops))
	for _, l := range r.loops {
		loops = append(loops, l)
	}
	r.mu.RUnlock()

	r.logger.Info("shutting down all loops", "count", len(loops))

	// Fire all cancellations in parallel (non-blocking).
	for _, l := range loops {
		l.cancel0()
	}

	// Wait for each loop to finish, respecting the context deadline.
	// Loops that were never started (Done()==nil) are treated as
	// already drained.
	stopped := 0
	for _, l := range loops {
		done := l.Done()
		if done == nil {
			// Never started — just deregister.
			stopped++
			r.Deregister(l.id)
			continue
		}
		select {
		case <-done:
			stopped++
			r.Deregister(l.id)
		case <-ctx.Done():
			r.logger.Warn("shutdown context expired, abandoning remaining loops",
				"stopped", stopped,
				"remaining", len(loops)-stopped,
			)
			return stopped
		}
	}

	r.logger.Info("all loops shut down", "stopped", stopped)
	return stopped
}

func (r *Registry) configureLoop(l *Loop, setup func(*Loop)) {
	// Call Setup before Register/Start so the caller can register
	// tools or perform other initialization that needs *Loop.
	if setup != nil {
		setup(l)
	}
}

func (r *Registry) startLoop(ctx context.Context, name string, l *Loop, setup func(*Loop), autoDeregister bool) error {
	r.configureLoop(l, setup)

	if err := r.Register(l); err != nil {
		return err
	}

	if err := l.Start(ctx); err != nil {
		r.Deregister(l.id)
		return fmt.Errorf("start loop %q: %w", name, err)
	}

	if autoDeregister {
		go func(id string, done <-chan struct{}) {
			<-done
			r.Deregister(id)
		}(l.id, l.Done())
	}

	return nil
}

// SpawnLoop creates a new loop with the given config, registers it, and
// starts it. This is the primary entry point for creating loops. Returns
// the loop ID on success.
func (r *Registry) SpawnLoop(ctx context.Context, cfg Config, deps Deps) (string, error) {
	l, err := New(cfg, deps)
	if err != nil {
		return "", fmt.Errorf("create loop %q: %w", cfg.Name, err)
	}
	if err := r.startLoop(ctx, cfg.Name, l, cfg.Setup, true); err != nil {
		return "", err
	}

	return l.id, nil
}

// SpawnSpec creates, registers, and starts a loop from a [Spec]. It is
// the additive loops-ng entrypoint while existing call sites continue to
// use [Registry.SpawnLoop] with [Config].
func (r *Registry) SpawnSpec(ctx context.Context, spec Spec, deps Deps) (string, error) {
	l, err := NewFromSpec(spec, deps)
	if err != nil {
		return "", fmt.Errorf("create loop %q: %w", spec.Name, err)
	}
	if err := r.startLoop(ctx, spec.Name, l, spec.ToConfig().Setup, true); err != nil {
		return "", err
	}

	return l.id, nil
}

// Launch starts a loops-ng [Launch]. Request/reply launches wait for
// completion and return a final status snapshot; background and service
// launches detach immediately and leave the loop running in the
// registry.
func (r *Registry) Launch(ctx context.Context, launch Launch, deps Deps) (LaunchResult, error) {
	if err := launch.Validate(); err != nil {
		return LaunchResult{}, err
	}

	spec := launch.Spec
	spec.Operation = effectiveOperation(spec.Operation)
	l, err := NewFromLaunch(launch, deps)
	if err != nil {
		return LaunchResult{}, fmt.Errorf("create loop %q: %w", spec.Name, err)
	}

	cfg := l.config
	detached := spec.Operation != OperationRequestReply
	if err := r.startLoop(ctx, spec.Name, l, cfg.Setup, detached); err != nil {
		return LaunchResult{}, err
	}

	result := LaunchResult{
		LoopID:    l.id,
		Operation: spec.Operation,
		Detached:  detached,
	}
	if detached {
		r.startDetachedCompletion(launch, l)
		return result, nil
	}

	finalStatus := make(chan Status, 1)
	go func() {
		<-l.Done()
		finalStatus <- l.Status()
		r.Deregister(l.id)
	}()

	waitCtx := ctx
	waitCancel := func() {}
	if launch.RunTimeout > 0 {
		waitCtx, waitCancel = context.WithTimeout(ctx, launch.RunTimeout)
	}
	defer waitCancel()

	select {
	case st := <-finalStatus:
		result.Response = l.lastResponseSnapshot()
		result.FinalStatus = &st
		return result, nil
	case <-waitCtx.Done():
		if launch.RunTimeout > 0 && waitCtx.Err() == context.DeadlineExceeded {
			l.Stop()
		}
		return result, waitCtx.Err()
	}
}

func (r *Registry) startDetachedCompletion(launch Launch, l *Loop) {
	if l == nil {
		return
	}
	if launch.Spec.Completion != CompletionConversation && launch.Spec.Completion != CompletionChannel {
		return
	}
	if l.deps.CompletionSink == nil {
		return
	}
	conversationID := strings.TrimSpace(launch.CompletionConversationID)
	channelTarget := CloneCompletionChannelTarget(launch.CompletionChannel)
	if conversationID == "" {
		if launch.Spec.Completion == CompletionConversation {
			return
		}
	}
	if channelTarget == nil && launch.Spec.Completion == CompletionChannel {
		return
	}

	go func() {
		done := l.Done()
		if done != nil {
			<-done
		}

		resp := l.lastResponseSnapshot()
		status := l.Status()
		content := formatCompletionContent(launch, resp, status)
		if strings.TrimSpace(content) == "" {
			return
		}

		deliveryCtx, cancel := context.WithTimeout(context.Background(), detachedCompletionTimeout)
		defer cancel()

		if err := l.deps.CompletionSink(deliveryCtx, CompletionDelivery{
			Mode:           launch.Spec.Completion,
			ConversationID: conversationID,
			Channel:        channelTarget,
			Content:        content,
			LoopID:         l.id,
			LoopName:       l.config.Name,
			Response:       resp,
			Status:         &status,
		}); err != nil {
			r.logger.Warn("detached loop completion delivery failed",
				"loop_id", l.id,
				"loop_name", l.config.Name,
				"delivery_mode", launch.Spec.Completion,
				"conversation_id", conversationID,
				"channel_target", channelTarget,
				"delivery_timeout", detachedCompletionTimeout,
				"error", err,
			)
			return
		}
		r.logger.Info("detached loop completion delivered",
			"loop_id", l.id,
			"loop_name", l.config.Name,
			"delivery_mode", launch.Spec.Completion,
			"conversation_id", conversationID,
			"channel_target", channelTarget,
		)
	}()
}

func formatCompletionContent(launch Launch, resp *Response, status Status) string {
	label := strings.TrimSpace(launch.Task)
	if label == "" {
		label = strings.TrimSpace(launch.Spec.Name)
	}
	if label == "" {
		label = "background task"
	}

	prefix := fmt.Sprintf("Background task complete (%s).", label)
	switch {
	case resp != nil && strings.TrimSpace(resp.Content) != "":
		return prefix + "\n\n" + resp.Content
	case status.LastError != "":
		return prefix + "\n\nTask failed: " + status.LastError
	case resp != nil && resp.Exhausted && resp.FinishReason != "":
		return prefix + "\n\nTask exhausted: " + resp.FinishReason
	default:
		return prefix
	}
}

// StopLoop stops a loop by ID and deregisters it once the goroutine
// has exited. Returns an error if the loop is not found. If the
// goroutine does not exit within 10 seconds (the Stop timeout), the
// loop remains registered to avoid orphaning a running goroutine.
func (r *Registry) StopLoop(id string) error {
	l := r.Get(id)
	if l == nil {
		return fmt.Errorf("loop %q not found", id)
	}

	l.Stop()

	// Only deregister if the goroutine actually exited. Stop() waits
	// up to 10s internally; if Done is closed, it exited cleanly.
	done := l.Done()
	if done == nil {
		// Never started — safe to deregister.
		r.Deregister(id)
		return nil
	}

	select {
	case <-done:
		r.Deregister(id)
	default:
		r.logger.Warn("loop goroutine still running after Stop, keeping registered",
			"loop_id", id,
			"loop_name", l.config.Name,
		)
	}

	return nil
}
