// Package audit writes admin-driven user-management events to audit_log.
// Thin wrapper over the sqlc-generated WriteAuditLog query — the value
// is centralizing the action-name vocabulary and the metadata
// marshaling so callers don't repeat boilerplate.
//
// Audit writes are best-effort from the caller's perspective: a failed
// audit write must NOT fail the user-facing operation. Callers
// log-and-continue. The audit log is observability, not a transaction
// participant.
package audit

import (
	"context"
	"encoding/json"
	"log/slog"

	"github.com/jackc/pgx/v5/pgtype"
	"github.com/jackc/pgx/v5/pgxpool"

	"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
)

// Action is the discriminator stored in audit_log.action. New values
// added in U2/U3 + future tasks; declare them here so callers don't
// stringly-type and so a future audit-search UI has a single source
// of truth for the vocabulary.
type Action string

const (
	// U1
	ActionRegister     Action = "register"
	ActionPromoteAdmin Action = "promote_admin"
	ActionDemoteAdmin  Action = "demote_admin"
	ActionInviteCreate Action = "invite_create"
	ActionInviteRedeem Action = "invite_redeem"
	ActionInviteRevoke Action = "invite_revoke"

	// U2 (declared early; callers in U1 don't use them yet, but
	// having them here means U2's diff is purely additive on the
	// caller side, not also touching this file).
	ActionCreateUserAdmin    Action = "create_user_admin"
	ActionDeleteUser         Action = "delete_user"
	ActionPasswordResetAdmin Action = "password_reset_admin"
	ActionAutoApproveToggle  Action = "auto_approve_toggle"

	// U3
	ActionPasswordChangeSelf   Action = "password_change_self"
	ActionTokenRegenerate      Action = "token_regenerate"
	ActionForgotPasswordInit   Action = "forgot_password_initiated"
	ActionPasswordResetByEmail Action = "password_reset_via_email"

	// Active-sessions surface (#370). Worth auditing rather than silent:
	// revoking sessions is what a user does when they think an account is
	// compromised, so the audit trail is most useful precisely when it's
	// exercised.
	ActionSessionRevoke       Action = "session_revoke"
	ActionSessionRevokeOthers Action = "session_revoke_others"
)

// Write inserts one audit_log row. metadata is marshaled as JSON;
// nil metadata writes SQL NULL. Errors are returned so callers can
// log them — but per package doc, callers should NOT fail user-facing
// operations on audit-write failures.
//
// actorID may be a zero/invalid pgtype.UUID for system actions
// (e.g. self-registration where the new user is both actor and
// target — pass them as the same id, or pass invalid for actor and
// the new user as target).
func Write(ctx context.Context, pool *pgxpool.Pool, actorID, targetID pgtype.UUID, action Action, metadata map[string]any) error {
	q := dbq.New(pool)
	var jsonMeta []byte
	if metadata != nil {
		b, err := json.Marshal(metadata)
		if err != nil {
			return err
		}
		jsonMeta = b
	}
	return q.WriteAuditLog(ctx, dbq.WriteAuditLogParams{
		ActorID:  actorID,
		TargetID: targetID,
		Action:   string(action),
		Metadata: jsonMeta,
	})
}

// WriteOrLog writes the audit row; on error, logs at Warn and swallows
// (audit failures must not break user-facing operations — see package doc).
// Use this when the audit is observability, not gating; use Write directly
// when the caller needs strict semantics (e.g. tests).
func WriteOrLog(ctx context.Context, pool *pgxpool.Pool, logger *slog.Logger, actorID, targetID pgtype.UUID, action Action, metadata map[string]any) {
	if err := Write(ctx, pool, actorID, targetID, action, metadata); err != nil {
		if logger != nil {
			logger.Warn("audit failed", "action", string(action), "err", err)
		}
	}
}
