// bootstrap_handler.go — public landing page for the first-run admin
// flow. While the server has no users and still holds the in-memory
// one-shot bootstrap token printed at startup, /bootstrap renders a
// form that takes a username + password and creates the first admin.
//
// The operator never sees or types the token: the server already has
// it in memory, so the UI handler uses it directly. The token printed
// to stderr remains a break-glass fallback for the JSON
// /api/bootstrap path.
//
// Routes (wired in server.go):
//
//	GET  /bootstrap → handleUIBootstrapGet
//	POST /bootstrap → handleUIBootstrapPost
//
// Both routes self-disable the moment a user row exists; subsequent
// hits redirect to /login.
package http

import (
	"log/slog"
	stdhttp "net/http"
	"time"

	"github.com/oklog/ulid/v2"

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

type bootstrapPage struct {
	Username string
	Error    string
}

func (s *Server) handleUIBootstrapGet(w stdhttp.ResponseWriter, r *stdhttp.Request) {
	if !s.bootstrapAvailable(r) {
		stdhttp.Redirect(w, r, "/login", stdhttp.StatusSeeOther)
		return
	}
	s.renderBootstrap(w, r, "", "")
}

func (s *Server) handleUIBootstrapPost(w stdhttp.ResponseWriter, r *stdhttp.Request) {
	if !s.bootstrapAvailable(r) {
		stdhttp.Redirect(w, r, "/login", stdhttp.StatusSeeOther)
		return
	}
	if err := r.ParseForm(); err != nil {
		stdhttp.Error(w, "bad request", stdhttp.StatusBadRequest)
		return
	}
	username := r.PostForm.Get("username")
	pw := r.PostForm.Get("password")
	pw2 := r.PostForm.Get("password_confirm")

	if username == "" {
		s.renderBootstrap(w, r, username, "Pick a username.")
		return
	}
	if pw == "" || pw2 == "" || pw != pw2 || len(pw) < 12 {
		s.renderBootstrap(w, r, username,
			"Passwords must match and be at least 12 characters.")
		return
	}

	hash, err := auth.HashPassword(pw)
	if err != nil {
		slog.Error("bootstrap: hash password", "err", err)
		stdhttp.Error(w, "internal", stdhttp.StatusInternalServerError)
		return
	}
	now := time.Now().UTC()
	u := store.User{
		ID:           ulid.Make().String(),
		Username:     username,
		PasswordHash: hash,
		Role:         store.RoleAdmin,
		CreatedAt:    now,
	}
	if err := s.deps.Store.CreateUser(r.Context(), u); err != nil {
		slog.Error("bootstrap: create user", "err", err)
		s.renderBootstrap(w, r, username,
			"Could not create the administrator account. Check the server logs.")
		return
	}
	// Clear the in-memory token so /api/bootstrap also stops accepting
	// further calls. CountUsers > 0 already gates both surfaces, but
	// blanking the token kills the constant-time-compare branch as
	// well — defence in depth, plus stops the token from sitting in
	// process memory longer than necessary.
	s.deps.BootstrapToken = ""

	_ = s.deps.Store.AppendAudit(r.Context(), store.AuditEntry{
		ID:     ulid.Make().String(),
		UserID: &u.ID,
		Actor:  "system",
		Action: "auth.bootstrap",
		TS:     now,
	})

	// Mint a session so the new admin lands authenticated on /.
	rawSession, err := auth.NewToken()
	if err != nil {
		slog.Error("bootstrap: session token", "err", err)
		stdhttp.Error(w, "internal", stdhttp.StatusInternalServerError)
		return
	}
	if err := s.deps.Store.CreateSession(r.Context(), store.Session{
		UserID:    u.ID,
		CreatedAt: now,
		ExpiresAt: now.Add(sessionTTL),
		IP:        r.RemoteAddr,
		UA:        r.UserAgent(),
	}, auth.HashToken(rawSession)); err != nil {
		slog.Error("bootstrap: create session", "err", err)
		stdhttp.Error(w, "internal", stdhttp.StatusInternalServerError)
		return
	}
	_ = s.deps.Store.MarkUserLogin(r.Context(), u.ID, now)

	stdhttp.SetCookie(w, &stdhttp.Cookie{
		Name:     sessionCookieName,
		Value:    rawSession,
		Path:     "/",
		HttpOnly: true,
		Secure:   s.deps.Cfg.CookieSecure,
		SameSite: stdhttp.SameSiteLaxMode,
		Expires:  now.Add(sessionTTL),
	})
	stdhttp.Redirect(w, r, "/", stdhttp.StatusSeeOther)
}

// bootstrapAvailable reports whether a fresh-install bootstrap can
// still proceed: a one-shot token is held in memory and no user rows
// exist yet.
func (s *Server) bootstrapAvailable(r *stdhttp.Request) bool {
	if s.deps.BootstrapToken == "" {
		return false
	}
	n, err := s.deps.Store.CountUsers(r.Context())
	if err != nil {
		slog.Error("bootstrap: count users", "err", err)
		return false
	}
	return n == 0
}

func (s *Server) renderBootstrap(w stdhttp.ResponseWriter, r *stdhttp.Request, username, errMsg string) {
	view := s.baseView(r, nil)
	view.Title = "Welcome · restic-manager"
	view.Page = bootstrapPage{Username: username, Error: errMsg}
	if err := s.deps.UI.Render(w, "bootstrap", view); err != nil {
		slog.Error("ui bootstrap: render", "err", err)
		stdhttp.Error(w, "internal", stdhttp.StatusInternalServerError)
	}
}
