package observability

import (
	"context"
	"database/sql"
	"sync/atomic"
	"time"
)

// HealthStatus is the JSON response shape served by /api/system/health.
type HealthStatus struct {
	Status        string    `json:"status"`
	Timestamp     int64     `json:"timestamp"`
	UptimeSeconds int64     `json:"uptime_seconds"`
	Version       string    `json:"version"`
	Checks        []Check   `json:"checks"`
}

// Check is one named subcomponent's status inside the overall HealthStatus.
type Check struct {
	Name    string `json:"name"`
	Status  string `json:"status"`            // "ok" | "degraded" | "unhealthy"
	Message string `json:"message,omitempty"`
}

const (
	StatusOK        = "ok"
	StatusDegraded  = "degraded"
	StatusUnhealthy = "unhealthy"
	// StatusDisabled marks a check whose subject is intentionally not in
	// play for this build / deployment (e.g. DuckDB compiled out with
	// -tags no_duckdb). It does NOT contribute to the rolled-up overall
	// status — the operator chose this configuration; reporting it as
	// "degraded" would falsely alarm the dashboard every page load.
	StatusDisabled = "disabled"
)

// HealthChecker owns the inputs each check examines: the two database
// handles, the secrets store key path, and the config-loaded flag.
//
// The checker is stateless beyond the inputs; Check() runs the probes on
// each invocation, with a per-probe timeout so a degraded SQLite or DuckDB
// does not stall the endpoint.
type HealthChecker struct {
	SQLite          *sql.DB
	DuckDB          *sql.DB
	SecretsKeyPath  string
	configLoaded    atomic.Bool
	startedAt       time.Time
	version         string
	duckDBAvailable bool
}

// NewHealthChecker constructs a checker. version is the build version string
// surfaced in the response; "dev" is a sensible default when no version is
// injected at build time.
func NewHealthChecker(sqlite, duck *sql.DB, secretsKeyPath, version string, duckdbAvailable bool) *HealthChecker {
	return &HealthChecker{
		SQLite:          sqlite,
		DuckDB:          duck,
		SecretsKeyPath:  secretsKeyPath,
		startedAt:       time.Now(),
		version:         version,
		duckDBAvailable: duckdbAvailable,
	}
}

// MarkConfigLoaded is called by the config Manager after the initial config
// loads successfully. Subsequent reloads do not flip this back to false; a
// failed reload simply keeps the previous valid config in effect.
func (h *HealthChecker) MarkConfigLoaded() { h.configLoaded.Store(true) }

// Check runs all probes and returns the rolled-up status.
//
// Aggregation rule: if any probe is unhealthy, overall is unhealthy. If any
// probe is degraded, overall is degraded. Otherwise overall is ok.
func (h *HealthChecker) Check(ctx context.Context) HealthStatus {
	pingCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
	defer cancel()

	checks := []Check{
		h.checkSQLite(pingCtx),
		h.checkDuckDB(pingCtx),
		h.checkSecrets(),
		h.checkConfig(),
	}

	status := StatusOK
	for _, c := range checks {
		switch c.Status {
		case StatusUnhealthy:
			status = StatusUnhealthy
		case StatusDegraded:
			if status != StatusUnhealthy {
				status = StatusDegraded
			}
		}
	}

	return HealthStatus{
		Status:        status,
		Timestamp:     time.Now().Unix(),
		UptimeSeconds: int64(time.Since(h.startedAt).Seconds()),
		Version:       h.version,
		Checks:        checks,
	}
}

func (h *HealthChecker) checkSQLite(ctx context.Context) Check {
	if h.SQLite == nil {
		return Check{Name: "sqlite", Status: StatusUnhealthy, Message: "not initialized"}
	}
	if err := h.SQLite.PingContext(ctx); err != nil {
		return Check{Name: "sqlite", Status: StatusUnhealthy, Message: err.Error()}
	}
	return Check{Name: "sqlite", Status: StatusOK}
}

func (h *HealthChecker) checkDuckDB(ctx context.Context) Check {
	if !h.duckDBAvailable {
		// Build-time choice (no_duckdb tag): SQLite carries all the data;
		// DuckDB is a secondary analytics store only. Report as `disabled`
		// so the overall status doesn't flip to degraded just for this.
		return Check{Name: "duckdb", Status: StatusDisabled, Message: "not compiled in — analytics on SQLite only (no_duckdb build tag)"}
	}
	if h.DuckDB == nil {
		return Check{Name: "duckdb", Status: StatusUnhealthy, Message: "not initialized"}
	}
	if err := h.DuckDB.PingContext(ctx); err != nil {
		return Check{Name: "duckdb", Status: StatusUnhealthy, Message: err.Error()}
	}
	return Check{Name: "duckdb", Status: StatusOK}
}

func (h *HealthChecker) checkSecrets() Check {
	if h.SecretsKeyPath == "" {
		return Check{Name: "age_key", Status: StatusUnhealthy, Message: "no key path"}
	}
	return Check{Name: "age_key", Status: StatusOK, Message: h.SecretsKeyPath}
}

func (h *HealthChecker) checkConfig() Check {
	if !h.configLoaded.Load() {
		return Check{Name: "config", Status: StatusUnhealthy, Message: "not loaded"}
	}
	return Check{Name: "config", Status: StatusOK}
}
