package http

import (
	"encoding/json"
	"net/http"
	"time"

	"github.com/denn-gubsky/loomcycle/internal/auth"
	"github.com/denn-gubsky/loomcycle/internal/retention"
)

// retentionReportResponse is the wire shape of GET /v1/_retention (RFC BM). It
// reports the data-retention sweeper's effective configuration plus — for an
// admin — a per-def-type count of currently-purgeable retired versions.
//
// The purgeable counts + the export_dir are ADMIN-ONLY: the counts span every
// tenant (the retention purge is an operator-global function, not tenant-scoped),
// so exposing them to a substrate:tenant operator would be a cross-tenant oracle;
// export_dir is a host filesystem path (infra detail). A tenant operator still
// sees the config knobs (so its UI can show that retention exists + how it's
// tuned), mirroring /v1/_routing's admin-vs-tenant split.
type retentionReportResponse struct {
	Admin         bool   `json:"admin"`
	Enabled       bool   `json:"enabled"`
	IntervalMS    int64  `json:"interval_ms"`
	DefsMode      string `json:"defs_mode"`
	DefsMaxAgeMS  int64  `json:"defs_max_age_ms"`
	DefsKeepLastN int    `json:"defs_keep_last_n"`
	// ChatsMode / ChatsMaxAgeMS are the RFC BM Phase 2 aged-chat archiver knobs
	// (config only — tenant-readable). ChatsMode reflects the effective value
	// after the legacy LOOMCYCLE_USAGE_RUN_RETENTION_* alias in config.Load.
	ChatsMode     string `json:"chats_mode"`
	ChatsMaxAgeMS int64  `json:"chats_max_age_ms"`
	// ChatsInternalMaxAgeMS is the shorter age applied to chats authored by an
	// agent declared `internal:` (the runtime's own maintenance plumbing; those
	// chats are deleted rather than exported). Reported EFFECTIVE — a configured
	// 0 means "inherit the global chat age", so this shows the age that actually
	// applies rather than the literal 0.
	ChatsInternalMaxAgeMS int64 `json:"chats_internal_max_age_ms"`
	// MemMode / MemMaxAgeMS are the RFC BM Phase 3 retired-agent memory-reclamation
	// knobs (config only — tenant-readable).
	MemMode     string `json:"mem_mode"`
	MemMaxAgeMS int64  `json:"mem_max_age_ms"`
	// MemContentMode / MemContentMaxAgeMS are the retired-entity-content prune knobs
	// — distinct from MemMode, which reclaims a retired AGENT's whole scope. This one
	// prunes superseded content out of scopes that are still live, and exempts
	// `evidential` content at any age. Reported so the report covers every configured
	// family: an operator reading a report that omits a family it has enabled would
	// reasonably conclude nothing is deleting their data.
	MemContentMode     string `json:"mem_content_mode"`
	MemContentMaxAgeMS int64  `json:"mem_content_max_age_ms"`
	ExportDir          string `json:"export_dir,omitempty"`
	// Purgeable is the per-def-type count of versions the CURRENT age + keep-last-N
	// settings would purge right now, plus an aged-chat-session count under "chats"
	// and a retired-agent memory-reclamation count under "mem" (all regardless of
	// mode — a preview). Admin-only.
	//
	// "chats" EXCLUDES chats authored by an `internal:` agent; those are counted
	// separately under "chats_internal" against their own shorter cutoff, so the
	// two numbers line up with the two passes the sweep actually makes.
	Purgeable map[string]int `json:"purgeable,omitempty"`
	// SQLMemGC surfaces the SEPARATE always-on SQL-Memory garbage collector (RFC AA
	// Phase 3d/3f — idle-TTL + size-budget eviction of durable scopes), so an
	// operator sees the full retention picture in one place. Admin-only (an
	// operator-global infra subsystem, like export_dir).
	SQLMemGC *sqlMemGCInfo `json:"sqlmem_gc,omitempty"`
}

// sqlMemGCInfo is the admin-only view of the SQL-Memory GC config. Enabled is
// true when either eviction trigger is armed (idle-TTL OR size-budget).
type sqlMemGCInfo struct {
	Enabled       bool  `json:"enabled"`
	ScopeTTLMS    int   `json:"scope_ttl_ms"`
	IntervalMS    int   `json:"interval_ms"`
	TotalMaxBytes int64 `json:"total_max_bytes"`
}

// handleRetentionReport serves GET /v1/_retention — the read-only view of the
// RFC BM data-retention sweeper. Tenant-readable config (see requiredScopeFor);
// the purgeable counts + export_dir are stripped for a non-admin caller. 503 when
// the server has no store (an empty/disabled report).
func (s *Server) handleRetentionReport(w http.ResponseWriter, r *http.Request) {
	admin := true
	if p, ok := auth.PrincipalFromContext(r.Context()); ok {
		admin = auth.HasScope(p.Scopes, auth.ScopeAdmin)
	}

	// Effective config, applying the same fallbacks retention.New does, so the
	// report shows what the sweeper actually runs with.
	interval := s.cfg.Env.RetentionInterval
	if interval <= 0 {
		interval = time.Hour
	}
	mode := s.cfg.Env.RetentionDefsMode
	if mode == "" {
		mode = "off"
	}
	chatsMode := s.cfg.Env.RetentionChatsMode
	if chatsMode == "" {
		chatsMode = "off"
	}
	memMode := s.cfg.Env.RetentionMemMode
	if memMode == "" {
		memMode = "off"
	}
	memContentMode := s.cfg.Env.RetentionMemContentMode
	if memContentMode == "" {
		memContentMode = "off"
	}
	// 0 = inherit the global chat age (never "delete immediately"), resolved the
	// same way retention.New resolves it.
	chatsInternalMaxAge := s.cfg.Env.RetentionChatsInternalMaxAge
	if chatsInternalMaxAge == 0 {
		chatsInternalMaxAge = s.cfg.Env.RetentionChatsMaxAge
	}
	resp := retentionReportResponse{
		Admin:                 admin,
		Enabled:               s.cfg.Env.RetentionEnabled,
		IntervalMS:            interval.Milliseconds(),
		DefsMode:              mode,
		DefsMaxAgeMS:          s.cfg.Env.RetentionDefsMaxAge.Milliseconds(),
		DefsKeepLastN:         s.cfg.Env.RetentionDefsKeepLastN,
		ChatsMode:             chatsMode,
		ChatsMaxAgeMS:         s.cfg.Env.RetentionChatsMaxAge.Milliseconds(),
		ChatsInternalMaxAgeMS: chatsInternalMaxAge.Milliseconds(),
		MemMode:               memMode,
		MemMaxAgeMS:           s.cfg.Env.RetentionMemMaxAge.Milliseconds(),
		MemContentMode:        memContentMode,
		MemContentMaxAgeMS:    s.cfg.Env.RetentionMemContentMaxAge.Milliseconds(),
	}

	if s.store == nil {
		// Read-only, degraded: report config only (no store → nothing to purge).
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusOK)
		_ = json.NewEncoder(w).Encode(resp)
		return
	}

	// Admin-only global detail: the export dir (infra path) + the cross-tenant
	// purgeable counts.
	if admin {
		resp.ExportDir = s.cfg.Env.RetentionExportDir
		rCfg := retention.Config{
			DefsMode:      mode,
			DefsMaxAge:    s.cfg.Env.RetentionDefsMaxAge,
			DefsKeepLastN: s.cfg.Env.RetentionDefsKeepLastN,
			ChatsMode:     chatsMode,
			ChatsMaxAge:   s.cfg.Env.RetentionChatsMaxAge,
			MemMode:       memMode,
			MemMaxAge:     s.cfg.Env.RetentionMemMaxAge,
			ExportDir:     s.cfg.Env.RetentionExportDir,
			// The preview splits chats by authoring agent exactly as the sweep
			// does, so "chats" vs "chats_internal" in the counts below reflect the
			// two cutoffs an operator actually gets.
			ChatsInternalMaxAge: s.cfg.Env.RetentionChatsInternalMaxAge,
			InternalAgents:      s.cfg.InternalAgentNames(),
		}
		// Feed the SQL-Memory manager so the "mem" preview can see which agents
		// have an SQL-Memory scope. Typed-nil guard (nil pointer → nil interface).
		if s.sqlMem != nil {
			rCfg.SQLMem = s.sqlMem
		}
		sw := retention.New(s.store, rCfg)
		counts, err := sw.DryRunCounts(r.Context())
		if err != nil {
			writeJSONError(w, http.StatusInternalServerError, "internal", err.Error())
			return
		}
		resp.Purgeable = counts

		// Surface the separate SQL-Memory GC (RFC AA) as infra context.
		gcInterval := s.cfg.Storage.SqlMemGCIntervalMS
		if gcInterval == 0 && (s.cfg.Storage.SqlMemScopeTTLMS > 0 || s.cfg.Storage.SqlMemTotalMaxBytes > 0) {
			gcInterval = int(time.Hour.Milliseconds()) // the sqlmem default
		}
		resp.SQLMemGC = &sqlMemGCInfo{
			Enabled:       s.cfg.Storage.SqlMemScopeTTLMS > 0 || s.cfg.Storage.SqlMemTotalMaxBytes > 0,
			ScopeTTLMS:    s.cfg.Storage.SqlMemScopeTTLMS,
			IntervalMS:    gcInterval,
			TotalMaxBytes: s.cfg.Storage.SqlMemTotalMaxBytes,
		}
	}

	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusOK)
	_ = json.NewEncoder(w).Encode(resp)
}
