// Package observability owns the Command Center's first-class
// self-observation primitives: the structured event log written to SQLite,
// and the health endpoint surfaced under /api/system. Both are required by
// the Phase 0 deliverables checklist.
package observability

import (
	"context"
	"database/sql"
	"encoding/json"
	"errors"
	"time"

	"github.com/rs/zerolog"
)

// Event levels. The system_events.level column is a free-text TEXT, but in
// practice we keep it to this small set so the API can filter by it.
const (
	LevelDebug = "debug"
	LevelInfo  = "info"
	LevelWarn  = "warn"
	LevelError = "error"
)

// EventRecorder writes structured events to both the zerolog logger and the
// `system_events` SQLite table, so that operators can query them via
// /api/system/events after restart.
//
// A single recorder is intended to be shared across all subsystems via
// dependency injection. Writes are best-effort: a SQLite failure is logged
// but does not propagate, because event-recording must not block the
// subsystem that generated the event.
type EventRecorder struct {
	db     *sql.DB
	logger zerolog.Logger
}

// NewEventRecorder wraps db and logger. db may be nil during early bootstrap;
// in that case events are logged only and the SQLite write is skipped.
func NewEventRecorder(db *sql.DB, logger zerolog.Logger) *EventRecorder {
	return &EventRecorder{
		db:     db,
		logger: logger.With().Str("component", "events").Logger(),
	}
}

// Record persists an event row and emits the equivalent structured log line.
// ctx may be nil. correlationID may be empty.
func (e *EventRecorder) Record(ctx context.Context, level, component, message string, fields map[string]any, correlationID string) {
	if level == "" {
		level = LevelInfo
	}
	now := time.Now().Unix()
	var ctxJSON sql.NullString
	if len(fields) > 0 {
		if b, err := json.Marshal(fields); err == nil {
			ctxJSON = sql.NullString{String: string(b), Valid: true}
		}
	}

	// Emit to the structured logger first; this path is always available.
	evt := e.logger.WithLevel(zerologLevelFor(level)).
		Str("event_component", component).
		Str("event_message", message)
	if correlationID != "" {
		evt = evt.Str("correlation_id", correlationID)
	}
	if ctxJSON.Valid {
		evt = evt.RawJSON("event_context", []byte(ctxJSON.String))
	}
	evt.Msg("system_event")

	if e.db == nil {
		return
	}

	if ctx == nil {
		ctx = context.Background()
	}
	var corr sql.NullString
	if correlationID != "" {
		corr = sql.NullString{String: correlationID, Valid: true}
	}
	_, err := e.db.ExecContext(ctx,
		`INSERT INTO system_events(timestamp, level, component, message, context_json, correlation_id)
		 VALUES (?, ?, ?, ?, ?, ?)`,
		now, level, component, message, ctxJSON, corr,
	)
	if err != nil {
		e.logger.Error().Err(err).Msg("failed to persist system event")
	}
}

// Query returns recent events filtered by level and component. Either or
// both may be empty to mean "no filter". limit is clamped to [1, 1000].
func (e *EventRecorder) Query(ctx context.Context, level, component string, limit int) ([]Event, error) {
	if e.db == nil {
		return nil, errors.New("events: db not initialized")
	}
	if limit <= 0 {
		limit = 100
	}
	if limit > 1000 {
		limit = 1000
	}

	q := `SELECT id, timestamp, level, component, message, context_json, correlation_id
	      FROM system_events`
	args := []any{}
	clauses := []string{}
	if level != "" {
		clauses = append(clauses, "level = ?")
		args = append(args, level)
	}
	if component != "" {
		clauses = append(clauses, "component = ?")
		args = append(args, component)
	}
	for i, c := range clauses {
		if i == 0 {
			q += " WHERE " + c
		} else {
			q += " AND " + c
		}
	}
	q += " ORDER BY timestamp DESC LIMIT ?"
	args = append(args, limit)

	rows, err := e.db.QueryContext(ctx, q, args...)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var out []Event
	for rows.Next() {
		var (
			ev      Event
			ctxJSON sql.NullString
			corr    sql.NullString
		)
		if err := rows.Scan(&ev.ID, &ev.Timestamp, &ev.Level, &ev.Component, &ev.Message, &ctxJSON, &corr); err != nil {
			return nil, err
		}
		if ctxJSON.Valid {
			ev.ContextJSON = ctxJSON.String
		}
		if corr.Valid {
			ev.CorrelationID = corr.String
		}
		out = append(out, ev)
	}
	return out, rows.Err()
}

// Event is the JSON shape returned by /api/system/events.
type Event struct {
	ID            int64  `json:"id"`
	Timestamp     int64  `json:"timestamp"`
	Level         string `json:"level"`
	Component     string `json:"component"`
	Message       string `json:"message"`
	ContextJSON   string `json:"context_json,omitempty"`
	CorrelationID string `json:"correlation_id,omitempty"`
}

func zerologLevelFor(s string) zerolog.Level {
	switch s {
	case LevelDebug:
		return zerolog.DebugLevel
	case LevelWarn:
		return zerolog.WarnLevel
	case LevelError:
		return zerolog.ErrorLevel
	default:
		return zerolog.InfoLevel
	}
}
