package api

import (
	"encoding/json"
	"errors"
	"net/http"

	"github.com/jackc/pgx/v5"
	"golang.org/x/crypto/bcrypt"

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

type resetPasswordReq struct {
	Token       string `json:"token"`
	NewPassword string `json:"new_password"`
}

// handleResetPassword implements POST /api/auth/reset-password.
//
// Atomically claims the reset token and writes the new password hash.
// Returns:
//   - 204 on successful reset
//   - 400 password_too_short if new_password < 8 chars
//   - 400 invalid_token if the token doesn't exist, was already used,
//     or has expired
//   - 500 on internal errors
func (h *handlers) handleResetPassword(w http.ResponseWriter, r *http.Request) {
	var req resetPasswordReq
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeErr(w, apierror.BadRequest("invalid_body", ""))
		return
	}
	if len(req.NewPassword) < minPasswordLength {
		writeErr(w, apierror.BadRequest("password_too_short", "password must be at least 8 chars"))
		return
	}
	if req.Token == "" {
		writeErr(w, apierror.BadRequest("invalid_token", ""))
		return
	}

	q := dbq.New(h.pool)

	// Look up the reset record so we know which user to update before
	// we claim the token. We can't reverse-lookup user_id after
	// UsePasswordReset (it returns rows-affected, not the row).
	reset, err := q.GetPasswordReset(r.Context(), req.Token)
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			writeErr(w, apierror.BadRequest("invalid_token", ""))
			return
		}
		h.logger.Error("reset password: lookup failed", "err", err)
		writeErr(w, apierror.Internal(err))
		return
	}

	// Atomically claim the token. Returns rows-affected (1 if
	// claimable, 0 if already used / expired). We do this BEFORE
	// hashing the password so concurrent reset attempts can't both
	// succeed.
	rows, err := q.UsePasswordReset(r.Context(), req.Token)
	if err != nil {
		h.logger.Error("reset password: use token failed", "err", err)
		writeErr(w, apierror.Internal(err))
		return
	}
	if rows == 0 {
		writeErr(w, apierror.BadRequest("invalid_token", ""))
		return
	}

	// Hash and update. If hashing or update fails after the token
	// claim, the token is "burned" but the password isn't reset —
	// user has to request another. That's acceptable; bcrypt and
	// UPDATE rarely fail in healthy systems.
	hash, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost)
	if err != nil {
		h.logger.Error("reset password: hash failed", "err", err)
		writeErr(w, apierror.Internal(err))
		return
	}
	if err := q.ChangeUserPassword(r.Context(), dbq.ChangeUserPasswordParams{
		ID:           reset.UserID,
		PasswordHash: string(hash),
	}); err != nil {
		h.logger.Error("reset password: update failed", "err", err)
		writeErr(w, apierror.Internal(err))
		return
	}

	audit.WriteOrLog(r.Context(), h.pool, h.logger, reset.UserID, reset.UserID, audit.ActionPasswordResetByEmail, nil)

	w.WriteHeader(http.StatusNoContent)
}
