package handlers

import (
	"net/http"
	"strconv"
	"time"

	"github.com/google/uuid"

	"mgit.msbls.de/m/paliad/internal/services"
)

// audit.go — admin-only global audit-log timeline (t-paliad-071).
//
// Both the page shell and the API endpoint are registered behind
// auth.RequireAdminFunc in handlers.go, so the in-handler logic can assume
// the caller is a global_admin and only validate the request shape.

// GET /api/audit-log — paginated, filterable timeline across paliad's four
// audit sources (project_events, caldav_sync_log, reminder_log,
// partner_unit_events).
//
// Query params:
//
//	source  — one of project_events, caldav_sync_log, reminder_log,
//	          partner_unit_events; empty = all
//	from    — ISO-8601 timestamp, inclusive lower bound
//	to      — ISO-8601 timestamp, inclusive upper bound
//	q       — free-text search (subject, description, title, event_type, actor)
//	before_ts / before_id — keyset cursor (timestamp + id of last seen row)
//	limit   — page size (default 50, capped at 200)
//
// Response shape: { entries: [...], next_cursor: { ts, id } | null }
func handleListAuditLog(w http.ResponseWriter, r *http.Request) {
	if !requireDB(w) {
		return
	}
	if dbSvc.audit == nil {
		writeJSON(w, http.StatusServiceUnavailable, map[string]string{
			"error": "audit service not configured",
		})
		return
	}

	q := r.URL.Query()
	filter := services.AuditFilter{
		Source: q.Get("source"),
		Search: q.Get("q"),
	}

	switch filter.Source {
	case "",
		services.AuditSourceProjectEvents,
		services.AuditSourceCalDAVLog,
		services.AuditSourceReminderLog,
		services.AuditSourcePartnerUnitEvents:
		// ok
	default:
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid source"})
		return
	}

	if v := q.Get("from"); v != "" {
		t, err := parseAuditTimestamp(v)
		if err != nil {
			writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid from"})
			return
		}
		filter.From = t
	}
	if v := q.Get("to"); v != "" {
		t, err := parseAuditTimestamp(v)
		if err != nil {
			writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid to"})
			return
		}
		filter.To = t
	}

	// Keyset cursor: both halves required to take effect. The frontend echoes
	// the next_cursor object verbatim, so a malformed pair is a programmer
	// error worth surfacing rather than silently falling back to page 1.
	beforeTSRaw := q.Get("before_ts")
	beforeIDRaw := q.Get("before_id")
	if (beforeTSRaw == "") != (beforeIDRaw == "") {
		writeJSON(w, http.StatusBadRequest, map[string]string{
			"error": "before_ts and before_id must be supplied together",
		})
		return
	}
	if beforeTSRaw != "" {
		t, err := parseAuditTimestamp(beforeTSRaw)
		if err != nil {
			writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid before_ts"})
			return
		}
		id, err := uuid.Parse(beforeIDRaw)
		if err != nil {
			writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid before_id"})
			return
		}
		filter.BeforeTS = &t
		filter.BeforeID = &id
	}

	if v := q.Get("limit"); v != "" {
		n, err := strconv.Atoi(v)
		if err != nil || n <= 0 {
			writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid limit"})
			return
		}
		filter.Limit = n
	}

	rows, err := dbSvc.audit.ListEntries(r.Context(), filter)
	if err != nil {
		writeServiceError(w, err)
		return
	}

	resp := map[string]any{
		"entries":     rows,
		"next_cursor": nil,
	}
	// "Has more" is implied by a full page — when the page is exactly the
	// requested limit, the next call needs the keyset of the last row. We
	// return nil otherwise so the client can stop paginating without a
	// dedicated total-count query.
	limit := filter.Limit
	if limit <= 0 {
		limit = services.DefaultAuditPageLimit
	}
	if len(rows) == limit {
		last := rows[len(rows)-1]
		resp["next_cursor"] = map[string]any{
			"ts": last.Timestamp,
			"id": last.ID,
		}
	}
	writeJSON(w, http.StatusOK, resp)
}

// parseAuditTimestamp accepts either a full RFC3339 timestamp or a bare
// YYYY-MM-DD date — the latter is what the <input type="date"> control
// produces, which the date-range filter uses for "last N days" presets.
// Date-only values are interpreted as midnight UTC; the from/to bounds are
// inclusive in the SQL, so a "to" date of 2026-04-29 catches everything up
// to the start of that day. The page renders an explicit hint so users know
// to pick "tomorrow" if they want today's rows.
func parseAuditTimestamp(s string) (time.Time, error) {
	if t, err := time.Parse(time.RFC3339, s); err == nil {
		return t, nil
	}
	if t, err := time.Parse("2006-01-02", s); err == nil {
		return t, nil
	}
	return time.Time{}, errInvalidTimestamp
}

var errInvalidTimestamp = &auditError{"invalid timestamp"}

type auditError struct{ msg string }

func (e *auditError) Error() string { return e.msg }

// handleAdminAuditLogPage serves the SPA shell for /admin/audit-log. The
// route is gated through RequireAdminFunc at registration; non-admins get
// the standard 302 to /dashboard?forbidden=admin.
func handleAdminAuditLogPage(w http.ResponseWriter, r *http.Request) {
	http.ServeFile(w, r, "dist/admin-audit-log.html")
}
