package auth

import (
	"context"
	"net/http"
	"regexp"
	"time"

	"nadir/internal/auditlog"

	"github.com/danielgtaylor/huma/v2"
)

// loginNameRe is the useradd default NAME_REGEX. Validating at this trust
// boundary keeps a flag-like name (e.g. "-c", "--help") from reaching `su` in
// showing up verbatim in audit logs / throttle keys.
var loginNameRe = regexp.MustCompile(`^[a-z_][a-z0-9_-]{0,31}\$?$`)

// authenticator verifies a username/password (PAM in production). It's a field
// of the login handler rather than a package global so tests can inject a stub
// without mutating shared state.
type authenticator func(username, password string) error

type LoginInput struct {
	Body struct {
		Username string `json:"username" doc:"System username"`
		Password string `json:"password" doc:"System password"`
	}
}

type LoginOutput struct {
	SetCookie http.Cookie `header:"Set-Cookie"`
	Body      struct {
		Status string `json:"status" example:"logged in"`
	}
}

// RegisterLogin wires the login operation into the Huma API. It has no
// permission metadata, so the RBAC middleware lets it through unauthenticated.
//
// secure sets the Secure attribute on the session cookie. Keep it true in
// production (the cookie is then only sent over HTTPS); set it false for local
// development over plain HTTP, where a Secure cookie would never be sent back.
func RegisterLogin(api huma.API, sessions *SessionStore, auditor *auditlog.Store, secure bool) {
	// loginThrottle blunts brute force: 5 failures for a username+source IP
	// trigger a one-minute cooldown. Persisted in SQLite so cooldowns survive
	// process restarts.
	registerLogin(api, sessions, auditor, secure, Authenticate, sessions.NewPersistentFailLimiter(5, time.Minute))
}

func registerLogin(api huma.API, sessions *SessionStore, auditor *auditlog.Store, secure bool, authenticate authenticator, throttle *failLimiter) {
	huma.Register(api, huma.Operation{
		OperationID: "login",
		Method:      "POST",
		Path:        "/api/login",
		Summary:     "Authenticate and start a session",
		Description: "Verifies the username and password against PAM (the " +
			"dedicated nadir service) and, on success, sets an HttpOnly " +
			"session cookie used to authorize all other endpoints.",
		Tags:   []string{"Authentication"},
		Errors: []int{401, 429},
	}, func(ctx context.Context, in *LoginInput) (*LoginOutput, error) {
		// Reject malformed usernames at the trust boundary so PAM, su, and the
		// audit log never see flag-like or shell-metacharacter input.
		if !loginNameRe.MatchString(in.Body.Username) {
			return nil, huma.Error401Unauthorized("invalid credentials")
		}
		// Throttle brute force: too many recent failures for this account/source
		// put it in a short cooldown before the password is even checked.
		throttleKey := in.Body.Username + "|" + ClientIP(ctx)
		if throttle.blocked(throttleKey) {
			return nil, huma.Error429TooManyRequests("too many failed login attempts; wait a minute")
		}
		// Record both outcomes: failed logins are the brute-force signal, and the
		// username is captured even on failure (which account is being targeted).
		if err := authenticate(in.Body.Username, in.Body.Password); err != nil {
			throttle.fail(throttleKey)
			auditor.Record(in.Body.Username, "POST", "/api/login", "auth", http.StatusUnauthorized)
			return nil, huma.Error401Unauthorized("invalid credentials")
		}
		throttle.reset(throttleKey)
		auditor.Record(in.Body.Username, "POST", "/api/login", "auth", http.StatusOK)

		sessionID, err := sessions.Create(in.Body.Username)
		if err != nil {
			return nil, huma.Error500InternalServerError("could not create session", err)
		}
		out := &LoginOutput{
		SetCookie: http.Cookie{
			Name:     "nadir_session_id",
			Value:    sessionID,
			Path:     "/",
			HttpOnly: true,
			Secure:   secure,
			SameSite: http.SameSiteStrictMode,
			MaxAge:   86400,
		},
		}
		out.Body.Status = "logged in"
		return out, nil
	})
}
