package daemon

import (
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"time"
)

// NotificationSlot tracks a pending notification for deduplication.
// Only the latest notification per slot matters - earlier ones are replaced.
type NotificationSlot struct {
	Slot      string    `json:"slot"`
	Session   string    `json:"session"`
	Message   string    `json:"message"`
	SentAt    time.Time `json:"sent_at"`
	Consumed  bool      `json:"consumed"`
	ConsumedAt time.Time `json:"consumed_at,omitempty"`
}

// NotificationManager handles slot-based notification deduplication.
// It ensures that for a given (session, slot) pair, only one notification
// is pending at a time. Sending a new notification to the same slot
// replaces the previous one.
type NotificationManager struct {
	stateDir string // Directory for slot state files
	maxAge   time.Duration // Max age before considering a slot stale
}

// NewNotificationManager creates a new notification manager.
// stateDir is where slot state files are stored (e.g., ~/gt/daemon/notifications/)
func NewNotificationManager(stateDir string, maxAge time.Duration) *NotificationManager {
	return &NotificationManager{
		stateDir: stateDir,
		maxAge:   maxAge,
	}
}

// slotPath returns the path to the slot state file.
func (m *NotificationManager) slotPath(session, slot string) string {
	// Sanitize session name (replace / with -)
	safeSession := session
	for i := range safeSession {
		if safeSession[i] == '/' {
			safeSession = safeSession[:i] + "-" + safeSession[i+1:]
		}
	}
	return filepath.Join(m.stateDir, fmt.Sprintf("slot-%s-%s.json", safeSession, slot))
}

// GetSlot reads the current state of a notification slot.
func (m *NotificationManager) GetSlot(session, slot string) (*NotificationSlot, error) {
	path := m.slotPath(session, slot)
	data, err := os.ReadFile(path)
	if err != nil {
		if os.IsNotExist(err) {
			return nil, nil // No slot state
		}
		return nil, err
	}

	var ns NotificationSlot
	if err := json.Unmarshal(data, &ns); err != nil {
		return nil, err
	}

	return &ns, nil
}

// ShouldSend checks if a notification should be sent for this slot.
// Returns true if:
// - No pending notification exists for this slot
// - The pending notification is stale (older than maxAge)
// - The pending notification was consumed
func (m *NotificationManager) ShouldSend(session, slot string) (bool, error) {
	ns, err := m.GetSlot(session, slot)
	if err != nil {
		return true, err // On error, allow sending
	}

	if ns == nil {
		return true, nil // No pending notification
	}

	if ns.Consumed {
		return true, nil // Previous was consumed
	}

	// Check if stale
	if time.Since(ns.SentAt) > m.maxAge {
		return true, nil // Stale, allow new send
	}

	return false, nil // Recent pending notification exists
}

// RecordSend records that a notification was sent for a slot.
func (m *NotificationManager) RecordSend(session, slot, message string) error {
	// Ensure directory exists
	if err := os.MkdirAll(m.stateDir, 0755); err != nil {
		return err
	}

	ns := &NotificationSlot{
		Slot:     slot,
		Session:  session,
		Message:  message,
		SentAt:   time.Now(),
		Consumed: false,
	}

	data, err := json.Marshal(ns)
	if err != nil {
		return err
	}

	return os.WriteFile(m.slotPath(session, slot), data, 0644)
}

// MarkConsumed marks a slot's notification as consumed (agent responded).
func (m *NotificationManager) MarkConsumed(session, slot string) error {
	ns, err := m.GetSlot(session, slot)
	if err != nil {
		return err
	}

	if ns == nil {
		return nil // Nothing to mark
	}

	ns.Consumed = true
	ns.ConsumedAt = time.Now()

	data, err := json.Marshal(ns)
	if err != nil {
		return err
	}

	return os.WriteFile(m.slotPath(session, slot), data, 0644)
}

// MarkSessionActive marks all slots for a session as consumed.
// Call this when the session shows activity (keepalive update).
func (m *NotificationManager) MarkSessionActive(session string) error {
	// List all slot files for this session
	pattern := filepath.Join(m.stateDir, fmt.Sprintf("slot-%s-*.json", session))
	matches, err := filepath.Glob(pattern)
	if err != nil {
		return err
	}

	for _, path := range matches {
		data, err := os.ReadFile(path)
		if err != nil {
			continue
		}

		var ns NotificationSlot
		if err := json.Unmarshal(data, &ns); err != nil {
			continue
		}

		if !ns.Consumed {
			ns.Consumed = true
			ns.ConsumedAt = time.Now()
			if data, err := json.Marshal(&ns); err == nil {
				_ = os.WriteFile(path, data, 0644) // non-fatal: state file update
			}
		}
	}

	return nil
}

// ClearSlot removes the state file for a slot.
func (m *NotificationManager) ClearSlot(session, slot string) error {
	path := m.slotPath(session, slot)
	err := os.Remove(path)
	if os.IsNotExist(err) {
		return nil
	}
	return err
}

// ClearStaleSlots removes slot files older than maxAge.
func (m *NotificationManager) ClearStaleSlots() error {
	pattern := filepath.Join(m.stateDir, "slot-*.json")
	matches, err := filepath.Glob(pattern)
	if err != nil {
		return err
	}

	for _, path := range matches {
		info, err := os.Stat(path)
		if err != nil {
			continue
		}

		if time.Since(info.ModTime()) > m.maxAge {
			_ = os.Remove(path) // best-effort cleanup
		}
	}

	return nil
}

// Common notification slots
const (
	SlotHeartbeat = "heartbeat"
	SlotStatus    = "status"
)
