// ui_audit.go — Audit log read-only surfaces.
//
// Routes (wired in server.go):
//
//	GET /audit         → handleUIAudit     (HTML)
//	GET /audit.csv     → handleUIAuditCSV  (CSV download honouring current filters)
//	GET /api/audit     → handleAPIAudit    (JSON)
//
// Filters: user, actor, action (substring), target_kind, time-range
// preset (24h | 7d | 30d | all). Page-level live refresh is *not*
// added here — audit is append-only and operators inspect history,
// not current state.
package http

import (
	"encoding/csv"
	"encoding/json"
	"fmt"
	"log/slog"
	stdhttp "net/http"
	"net/url"
	"strings"
	"time"

	"gitea.dcglab.co.uk/steve/restic-manager/internal/store"
)

type auditPage struct {
	Filter    store.AuditFilter
	Range     string // "24h" | "7d" | "30d" | "all"
	Entries   []store.AuditEntry
	UserNames map[string]string // user_id → username for row rendering
	HostNames map[string]string // host_id → name (for target_kind=host display)
	Actions   []string          // distinct actions seen so far, for the dropdown
	// Sort + Dir reflect the *resolved* sort (after allowlist
	// validation) so the template can render arrows on the active
	// column.
	Sort string // "ts" | "actor" | "user_id" | "action" | "target_kind"
	Dir  string // "asc" | "desc"
	// SortHrefs is a fully-encoded /audit?…&sort=COL&dir=… for each
	// sortable column. Built server-side because constructing the
	// querystring inside a Go html/template <a href="…"> applies
	// URL-attribute escaping to '=' (turning 'range=all' into
	// 'range%3dall' on the wire), which loses every filter on click.
	// CSVHref is the analogous link for the export button.
	SortHrefs map[string]string
	CSVHref   string
}

// rangeToSince converts the time-range preset to a Since cutoff. "all"
// (or unrecognised) returns the zero time, meaning "no lower bound".
func rangeToSince(r string, now time.Time) time.Time {
	switch r {
	case "24h", "":
		return now.Add(-24 * time.Hour)
	case "7d":
		return now.Add(-7 * 24 * time.Hour)
	case "30d":
		return now.Add(-30 * 24 * time.Hour)
	default:
		return time.Time{}
	}
}

// auditFilterFromQuery extracts the AuditFilter + range preset from
// the request querystring. Shared by the HTML, CSV, and JSON handlers
// so all three honour the same filter URL.
func auditFilterFromQuery(r *stdhttp.Request) (store.AuditFilter, string) {
	q := r.URL.Query()
	rng := q.Get("range")
	if rng == "" {
		rng = "24h"
	}
	return store.AuditFilter{
		UserID:     q.Get("user_id"),
		Actor:      q.Get("actor"),
		ActionLike: strings.TrimSpace(q.Get("action")),
		TargetKind: q.Get("target_kind"),
		Since:      rangeToSince(rng, time.Now().UTC()),
		Limit:      5000, // CSV export tolerates more rows; HTML clamps via paging later
		OrderBy:    q.Get("sort"),
		OrderAsc:   q.Get("dir") == "asc",
	}, rng
}

func (s *Server) handleUIAudit(w stdhttp.ResponseWriter, r *stdhttp.Request) {
	u := s.requireUIUser(w, r)
	if u == nil {
		return
	}
	f, rng := auditFilterFromQuery(r)
	// HTML page caps lower than CSV — keeps the table snappy.
	if f.Limit > 500 {
		f.Limit = 500
	}

	entries, err := s.deps.Store.ListAudit(r.Context(), f)
	if err != nil {
		slog.Error("ui audit: list", "err", err)
		stdhttp.Error(w, "internal", stdhttp.StatusInternalServerError)
		return
	}

	// Resolve the sort key once so the page model and the template
	// see the same value the SQL just used. f.OrderBy may have been
	// '' or unknown → 'ts'; the template needs the resolved one.
	resolvedSort := "ts"
	switch f.OrderBy {
	case "actor", "user_id", "action", "target_kind":
		resolvedSort = f.OrderBy
	}
	dir := "desc"
	if f.OrderAsc {
		dir = "asc"
	}
	// Build the per-column sort hrefs once, so the template only
	// has to emit them. Each click flips dir on the active column;
	// any other column starts at desc (newest-first / Z→A).
	base := url.Values{}
	if rng != "" {
		base.Set("range", rng)
	}
	if f.UserID != "" {
		base.Set("user_id", f.UserID)
	}
	if f.Actor != "" {
		base.Set("actor", f.Actor)
	}
	if f.ActionLike != "" {
		base.Set("action", f.ActionLike)
	}
	if f.TargetKind != "" {
		base.Set("target_kind", f.TargetKind)
	}
	csvHref := "/audit.csv?" + base.Encode()
	hrefs := make(map[string]string, 5)
	for _, col := range []string{"ts", "actor", "user_id", "action", "target_kind"} {
		v := url.Values{}
		for k, vs := range base {
			v[k] = vs
		}
		v.Set("sort", col)
		newDir := "desc"
		if col == resolvedSort && dir == "desc" {
			newDir = "asc"
		}
		v.Set("dir", newDir)
		hrefs[col] = "/audit?" + v.Encode()
	}

	page := auditPage{
		Filter:    f,
		Range:     rng,
		Entries:   entries,
		UserNames: map[string]string{},
		HostNames: map[string]string{},
		Sort:      resolvedSort,
		Dir:       dir,
		SortHrefs: hrefs,
		CSVHref:   csvHref,
	}
	if users, err := s.deps.Store.ListUsers(r.Context(), store.UserSort{}); err == nil {
		for _, ux := range users {
			page.UserNames[ux.ID] = ux.Username
		}
	}
	if hosts, err := s.deps.Store.ListHosts(r.Context()); err == nil {
		for _, h := range hosts {
			page.HostNames[h.ID] = h.Name
		}
	}
	if actions, err := s.deps.Store.DistinctAuditActions(r.Context()); err == nil {
		page.Actions = actions
	}

	view := s.baseView(r, u)
	view.Title = "Audit · restic-manager"
	view.Active = "audit"
	view.Page = page
	if err := s.deps.UI.Render(w, "audit", view); err != nil {
		slog.Error("ui audit: render", "err", err)
	}
}

// handleAPIAudit is the JSON variant — same filters as the HTML page.
func (s *Server) handleAPIAudit(w stdhttp.ResponseWriter, r *stdhttp.Request) {
	if _, ok := s.requireUser(r); !ok {
		writeJSONError(w, stdhttp.StatusUnauthorized, "unauthorised", "")
		return
	}
	f, _ := auditFilterFromQuery(r)
	if f.Limit > 500 {
		f.Limit = 500
	}
	entries, err := s.deps.Store.ListAudit(r.Context(), f)
	if err != nil {
		writeJSONError(w, stdhttp.StatusInternalServerError, "internal", err.Error())
		return
	}
	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	_ = json.NewEncoder(w).Encode(map[string]any{"entries": entries})
}

// handleUIAuditCSV streams the filtered audit log as CSV. Auth-gated
// like the HTML page; honours the same filter querystring so an
// operator can refine the view in the browser, hit Export, and get
// exactly what's on screen (plus more rows up to the 5000 cap).
func (s *Server) handleUIAuditCSV(w stdhttp.ResponseWriter, r *stdhttp.Request) {
	u := s.requireUIUser(w, r)
	if u == nil {
		return
	}
	f, _ := auditFilterFromQuery(r)
	entries, err := s.deps.Store.ListAudit(r.Context(), f)
	if err != nil {
		stdhttp.Error(w, "internal", stdhttp.StatusInternalServerError)
		return
	}

	// Resolve user_id → username and host_id → name once for the
	// human-friendly columns.
	userNames := map[string]string{}
	if users, err := s.deps.Store.ListUsers(r.Context(), store.UserSort{}); err == nil {
		for _, ux := range users {
			userNames[ux.ID] = ux.Username
		}
	}
	hostNames := map[string]string{}
	if hosts, err := s.deps.Store.ListHosts(r.Context()); err == nil {
		for _, h := range hosts {
			hostNames[h.ID] = h.Name
		}
	}

	stamp := time.Now().UTC().Format("20060102-150405")
	w.Header().Set("Content-Type", "text/csv; charset=utf-8")
	w.Header().Set("Content-Disposition",
		fmt.Sprintf(`attachment; filename="audit-%s.csv"`, stamp))

	cw := csv.NewWriter(w)
	defer cw.Flush()
	// user_id and target_id are internal ULIDs that carry no meaning
	// to anyone reading the CSV — the resolved name (or — for system
	// rows / non-host targets) is what an operator wants. The HTML
	// page still shows IDs in the Target column for traceability when
	// no name is available; the CSV is for human reporting only.
	_ = cw.Write([]string{"timestamp_utc", "actor", "user", "action", "target_kind", "target_name", "payload"})
	for _, e := range entries {
		var uname string
		if e.UserID != nil {
			uname = userNames[*e.UserID]
		}
		var tk, tname string
		if e.TargetKind != nil {
			tk = *e.TargetKind
		}
		if tk == "host" && e.TargetID != nil {
			tname = hostNames[*e.TargetID]
		}
		payload := ""
		if len(e.Payload) > 0 {
			payload = string(e.Payload)
		}
		_ = cw.Write([]string{
			e.TS.UTC().Format("2006-01-02 15:04:05"),
			e.Actor, uname, e.Action, tk, tname, payload,
		})
	}
}
