// Package watcher detects when a Claude Code session has finished its current
// task and signals the dispatcher to assign a new one.
package watcher

import (
	"context"
	"log"
	"os"
	"path/filepath"
	"regexp"
	"strings"
	"time"

	"forge.secuaas.ovh/olivier/claude-failover/internal/config"
	"forge.secuaas.ovh/olivier/claude-failover/internal/state"
	"forge.secuaas.ovh/olivier/claude-failover/internal/tmux"
)

// spinnerRe matches Claude Code's "Xs ·" or "Xs ⠋" progress indicator.
var spinnerRe = regexp.MustCompile(`\d+s\s+[·⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏]`)

// SessionWatcher monitors active tmux sessions and emits on DoneChan when
// a Claude Code session returns to the idle prompt (❯) or exceeds its timeout.
type SessionWatcher struct {
	tmux        tmux.Client
	state       *state.State
	config      *config.Config
	done        chan string
	interval    time.Duration
	idleTimeout time.Duration
	signalDir   string
	logger      *log.Logger
}

// New creates a SessionWatcher with defaults from cfg.
func New(tc tmux.Client, s *state.State, cfg *config.Config) *SessionWatcher {
	interval := cfg.Watcher.Interval.Duration
	if interval == 0 {
		interval = 30 * time.Second
	}
	idleTimeout := cfg.Watcher.IdleTimeout.Duration
	if idleTimeout == 0 {
		idleTimeout = 60 * time.Minute
	}
	signalDir := cfg.Watcher.DoneSignalDir
	if signalDir == "" {
		signalDir = "/tmp"
	}
	return &SessionWatcher{
		tmux:        tc,
		state:       s,
		config:      cfg,
		done:        make(chan string, 32),
		interval:    interval,
		idleTimeout: idleTimeout,
		signalDir:   signalDir,
		logger:      log.Default(),
	}
}

// DoneChan returns the channel on which completed session names are sent.
func (w *SessionWatcher) DoneChan() <-chan string {
	return w.done
}

// Run starts the watcher loop until ctx is cancelled.
func (w *SessionWatcher) Run(ctx context.Context) {
	ticker := time.NewTicker(w.interval)
	defer ticker.Stop()
	for {
		select {
		case <-ctx.Done():
			return
		case <-ticker.C:
			w.poll()
		}
	}
}

// poll inspects all currently-working sessions once.
func (w *SessionWatcher) poll() {
	w.state.ForEachWorking(func(name string, sess *state.SessionState) {
		w.checkSession(name, sess)
	})
}

// checkSession evaluates a single working session for completion or timeout.
func (w *SessionWatcher) checkSession(name string, sess *state.SessionState) {
	// 1. Check the done-signal file written by hooks or external scripts.
	sigFile := filepath.Join(w.signalDir, "agent-done-"+name)
	if _, err := os.Stat(sigFile); err == nil {
		w.completeSession(name, sigFile)
		return
	}

	// 2. Capture the last 5 pane lines.
	tail, err := w.tmux.CapturePaneTail(name, 5)
	if err != nil {
		// Session may have vanished; lifecycle.Manager handles recreation.
		return
	}

	// 3. Idle prompt ❯ without an active spinner → Claude has finished.
	if hasClaudePrompt(tail) && !hasSpinner(tail) {
		w.completeSession(name, sigFile)
		return
	}

	// 4. Idle-timeout guard.
	if sess.AssignedAt != nil && time.Since(*sess.AssignedAt) > w.idleTimeout {
		w.logger.Printf("[watcher] TIMEOUT session=%q elapsed=%v idleTimeout=%v",
			name, time.Since(*sess.AssignedAt).Round(time.Second), w.idleTimeout)
		w.completeSession(name, sigFile)
	}
}

// completeSession sends /exit, marks the session idle, and notifies the dispatcher.
func (w *SessionWatcher) completeSession(name, sigFile string) {
	w.logger.Printf("[watcher] DONE session=%q → /exit", name)
	_ = w.tmux.SendKeys(name, "/exit")
	time.Sleep(500 * time.Millisecond)
	w.state.SetIdle(name)
	os.Remove(sigFile)
	select {
	case w.done <- name:
	default:
		w.logger.Printf("[watcher] done channel full, dropping signal for %q", name)
	}
}

// hasClaudePrompt returns true if the Claude Code interactive prompt is visible.
func hasClaudePrompt(output string) bool {
	return strings.Contains(output, "❯")
}

// hasSpinner returns true if Claude Code's progress spinner is active.
func hasSpinner(output string) bool {
	return spinnerRe.MatchString(output)
}
