package api

import (
	"context"
	"crypto/rand"
	"encoding/hex"
	"encoding/json"
	"errors"
	"net/http"
	"strings"
	"time"

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

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

const (
	resetTokenTTL  = 24 * time.Hour
	resetTokenSize = 32 // bytes; 64 chars hex on the wire
)

type forgotPasswordReq struct {
	Email string `json:"email"`
}

// handleForgotPassword implements POST /api/auth/forgot-password.
//
// Always returns 200 with an empty JSON body so the response shape
// doesn't reveal whether the email is registered. The actual side
// effect (token + email send) only happens when the email matches a
// user with email-on-file. SMTP send failures are logged but don't
// propagate to the response.
//
// Audits ActionForgotPasswordInit with metadata.email_match so the
// operator can correlate audit log entries with successful sends
// even though the response is opaque.
func (h *handlers) handleForgotPassword(w http.ResponseWriter, r *http.Request) {
	var req forgotPasswordReq
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		// Decode failures are also opaque — return 200 even on bad JSON.
		writeJSON(w, http.StatusOK, struct{}{})
		return
	}
	email := strings.ToLower(strings.TrimSpace(req.Email))

	q := dbq.New(h.pool)
	var matched bool
	var auditTarget pgtype.UUID
	if email != "" {
		user, err := q.GetUserByEmail(r.Context(), email)
		if err == nil && user.Email != nil && *user.Email != "" {
			matched = true
			auditTarget = user.ID
			if sendErr := h.sendResetEmail(r.Context(), r, user); sendErr != nil {
				h.logger.Warn("forgot-password: send failed",
					"email", email, "err", sendErr)
				// fall through; response is still 200
			}
		} else if err != nil && !errors.Is(err, pgx.ErrNoRows) {
			h.logger.Warn("forgot-password: lookup failed", "err", err)
		}
	}

	// Audit (best-effort).
	audit.WriteOrLog(r.Context(), h.pool, h.logger, pgtype.UUID{}, auditTarget, audit.ActionForgotPasswordInit,
		map[string]any{"email_match": matched})

	writeJSON(w, http.StatusOK, struct{}{})
}

// sendResetEmail generates a token, persists it, renders + sends the
// email. Returns the underlying error (caller logs it but doesn't
// surface to the HTTP response).
func (h *handlers) sendResetEmail(ctx context.Context, r *http.Request, user dbq.User) error {
	if h.mailer == nil {
		return errors.New("forgot-password: no mailer configured")
	}
	tokenBytes := make([]byte, resetTokenSize)
	if _, err := rand.Read(tokenBytes); err != nil {
		return err
	}
	token := hex.EncodeToString(tokenBytes)
	expiresAt := time.Now().Add(resetTokenTTL)

	q := dbq.New(h.pool)
	if err := q.CreatePasswordReset(ctx, dbq.CreatePasswordResetParams{
		Token:     token,
		UserID:    user.ID,
		ExpiresAt: pgtype.Timestamptz{Time: expiresAt, Valid: true},
	}); err != nil {
		return err
	}

	resetURL := buildResetURL(r, token)
	textBody, htmlBody, err := mailer.RenderResetEmail(mailer.ResetEmailVars{
		Username:    user.Username,
		ResetURL:    resetURL,
		ExpiryHours: int(resetTokenTTL.Hours()),
	})
	if err != nil {
		return err
	}

	if user.Email == nil || *user.Email == "" {
		// Defensive — sendResetEmail is only called when email is set,
		// but we re-check to avoid sending to nil.
		return errors.New("forgot-password: user has no email")
	}
	return h.mailer.Send(ctx, *user.Email, mailer.ResetEmailSubject, textBody, htmlBody)
}

func buildResetURL(r *http.Request, token string) string {
	scheme := "http"
	if r.TLS != nil {
		scheme = "https"
	}
	host := r.Host
	if host == "" {
		host = "localhost"
	}
	return scheme + "://" + host + "/reset-password/" + token
}
