package daemon

import (
	"strings"
	"sync"
	"time"
)

// StartupLogEntry records a startup event (success or failure) with diagnostic context.
type StartupLogEntry struct {
	ProcessID  string    `json:"process_id"`
	ScriptName string    `json:"script_name"` // Name without project prefix
	Level      string    `json:"level"`       // "info", "warning", "error"
	EventType  string    `json:"event_type"`  // "started", "EADDRINUSE", "start_failed", "cleanup_failed", "port_cleanup", "dependency_wait", "skipped"
	Message    string    `json:"message"`
	Output     string    `json:"output,omitempty"` // Last lines of process output (errors only)
	Port       int       `json:"port,omitempty"`
	Timestamp  time.Time `json:"timestamp"`
}

// StartupLogStore is a ring buffer of recent startup events.
// Survives process cleanup so logs remain queryable after processes
// are removed from the registry.
type StartupLogStore struct {
	mu      sync.RWMutex
	entries []*StartupLogEntry
	head    int
	len     int
	maxSize int
}

// NewStartupLogStore creates a new store with the given capacity.
func NewStartupLogStore(maxSize int) *StartupLogStore {
	if maxSize <= 0 {
		maxSize = 100
	}
	return &StartupLogStore{
		entries: make([]*StartupLogEntry, maxSize),
		maxSize: maxSize,
	}
}

// Add inserts a startup log entry into the ring buffer.
func (s *StartupLogStore) Add(entry *StartupLogEntry) {
	if entry == nil {
		return
	}
	s.mu.Lock()
	defer s.mu.Unlock()

	idx := (s.head + s.len) % s.maxSize
	s.entries[idx] = entry
	if s.len < s.maxSize {
		s.len++
	} else {
		s.head = (s.head + 1) % s.maxSize
	}
}

// Info adds an informational log entry.
func (s *StartupLogStore) Info(processID, scriptName, eventType, message string) {
	s.Add(&StartupLogEntry{
		ProcessID:  processID,
		ScriptName: scriptName,
		Level:      "info",
		EventType:  eventType,
		Message:    message,
		Timestamp:  time.Now(),
	})
}

// Error adds an error log entry.
func (s *StartupLogStore) Error(processID, scriptName, eventType, message string) {
	s.Add(&StartupLogEntry{
		ProcessID:  processID,
		ScriptName: scriptName,
		Level:      "error",
		EventType:  eventType,
		Message:    message,
		Timestamp:  time.Now(),
	})
}

// startupLogger binds a StartupLogStore to a single project path so every
// entry it records is automatically stamped with that project's ProcessID
// prefix (makeProcessID(projectPath, scriptName)).
//
// Project scoping of the startup log is a structural property of this type:
// the default startup_log / get_errors query is project-scoped and filters
// by the "basename-hash:" ProcessID prefix (see StartupLogFilter.ProjectPath).
// The raw store helpers take a free-form ProcessID, which made it easy to
// record a project event with an empty ProcessID — those entries silently
// failed the prefix match and were invisible to every non-global query. The
// logger removes that footgun: callers pass only the script/proxy name (or ""
// for a project-level event) and the project stamp is applied unconditionally.
// makeProcessID(projectPath, "") yields exactly the scoped prefix, so even
// nameless project-level events remain visible to a scoped query.
//
// Genuinely daemon-wide events (shutdown, orphan scans) that must remain
// global-only deliberately bypass this logger and call the store directly with
// no project path.
type startupLogger struct {
	store       *StartupLogStore
	projectPath string
}

// startupLog returns a project-bound logger over the daemon's startup log store.
func (d *Daemon) startupLog(projectPath string) *startupLogger {
	return &startupLogger{store: d.startupErrorStore, projectPath: projectPath}
}

func (l *startupLogger) record(level, name, eventType, message string, port int) {
	if l == nil || l.store == nil {
		return
	}
	l.store.Add(&StartupLogEntry{
		ProcessID:  makeProcessID(l.projectPath, name),
		ScriptName: name,
		Level:      level,
		EventType:  eventType,
		Message:    message,
		Port:       port,
		Timestamp:  time.Now(),
	})
}

// Info, Warn, Error record a project-stamped entry. name is the script/proxy
// name, or "" for a project-level event (still scoped via the project prefix).
func (l *startupLogger) Info(name, eventType, message string) {
	l.record("info", name, eventType, message, 0)
}
func (l *startupLogger) Warn(name, eventType, message string) {
	l.record("warning", name, eventType, message, 0)
}
func (l *startupLogger) Error(name, eventType, message string) {
	l.record("error", name, eventType, message, 0)
}

// WarnPort and ErrorPort record a port-bearing entry (port conflicts,
// listen-port clashes) so the offending port survives into the log.
func (l *startupLogger) WarnPort(name, eventType, message string, port int) {
	l.record("warning", name, eventType, message, port)
}
func (l *startupLogger) ErrorPort(name, eventType, message string, port int) {
	l.record("error", name, eventType, message, port)
}

// StartupLogFilter controls which entries are returned by Query.
type StartupLogFilter struct {
	Since     time.Time
	ProcessID string
	Level     string // "info", "warning", "error", or "" for all
	Limit     int

	// ProjectPath scopes results to a single project. Startup log entries
	// are not stamped with a project path at ingest, but their ProcessID
	// is built by makeProcessID(projectPath, name) which deterministically
	// encodes the project as a "basename-hash:" prefix. When set, only
	// entries whose ProcessID carries that prefix are returned. Entries
	// with a bare (non-project) ProcessID — daemon-wide shutdown/scan
	// events — are therefore visible only to a global (unscoped) query.
	// Empty means no project scoping (the global/legacy behaviour).
	ProjectPath string
}

// Query returns matching entries ordered oldest to newest.
func (s *StartupLogStore) Query(filter StartupLogFilter) []*StartupLogEntry {
	s.mu.RLock()
	defer s.mu.RUnlock()

	// Derive the deterministic "basename-hash:" ProcessID prefix for the
	// scoped project once, outside the loop. makeProcessID with an empty
	// name yields exactly that prefix.
	var projectPrefix string
	if filter.ProjectPath != "" {
		projectPrefix = makeProcessID(filter.ProjectPath, "")
	}

	var result []*StartupLogEntry
	for i := 0; i < s.len; i++ {
		entry := s.entries[(s.head+i)%s.maxSize]
		if entry == nil {
			continue
		}
		if !filter.Since.IsZero() && entry.Timestamp.Before(filter.Since) {
			continue
		}
		if filter.ProcessID != "" && entry.ProcessID != filter.ProcessID {
			continue
		}
		if projectPrefix != "" && !strings.HasPrefix(entry.ProcessID, projectPrefix) {
			continue
		}
		if filter.Level != "" && entry.Level != filter.Level {
			continue
		}
		result = append(result, entry)
	}

	if filter.Limit > 0 && len(result) > filter.Limit {
		result = result[len(result)-filter.Limit:]
	}
	return result
}

// Recent returns entries from the last duration, up to limit.
func (s *StartupLogStore) Recent(d time.Duration, limit int) []*StartupLogEntry {
	return s.Query(StartupLogFilter{
		Since: time.Now().Add(-d),
		Limit: limit,
	})
}

// Clear removes all entries.
func (s *StartupLogStore) Clear() {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.head = 0
	s.len = 0
}

// Len returns the current number of entries.
func (s *StartupLogStore) Len() int {
	s.mu.RLock()
	defer s.mu.RUnlock()
	return s.len
}
