package api

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

	"archivmail/internal/audit"
	"archivmail/internal/mailer"
	"archivmail/internal/tokenstore"
	"archivmail/internal/userstore"
)

// handleSignup creates an inactive account and sends a verification email.
// POST /api/auth/signup  (PROJ-28)
func (s *Server) handleSignup(w http.ResponseWriter, r *http.Request) {
	if !s.mailer.IsConfigured() {
		writeError(w, http.StatusServiceUnavailable, "E-Mail-Versand nicht konfiguriert (smtp_out)")
		return
	}

	var body struct {
		Username string `json:"username"`
		Email    string `json:"email"`
		Password string `json:"password"`
		Invite   string `json:"invite"` // required invite token
	}
	if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
		writeError(w, http.StatusBadRequest, "invalid body")
		return
	}
	if body.Invite == "" {
		writeError(w, http.StatusBadRequest, "Registrierung nur mit gültigem Einladungslink möglich")
		return
	}
	if body.Username == "" || body.Email == "" || body.Password == "" {
		writeError(w, http.StatusBadRequest, "username, email and password required")
		return
	}
	if len(body.Password) < 8 {
		writeError(w, http.StatusBadRequest, "password must be at least 8 characters")
		return
	}

	if s.tokenStore == nil {
		writeError(w, http.StatusInternalServerError, "token store not available")
		return
	}

	// Consume the invite token atomically BEFORE creating the user.
	// Using Use (not Peek) here prevents TOCTOU: two parallel requests with the same
	// token both pass Peek but only one wins the Use, ensuring one-time use.
	// The token is consumed regardless of whether user creation succeeds — this also
	// prevents account enumeration via "does this invite still work?" probing.
	inviteTok, err := s.tokenStore.Use(r.Context(), tokenstore.TypeInvite, body.Invite)
	if err != nil {
		writeError(w, http.StatusBadRequest, "ungültiger oder abgelaufener Einladungslink")
		return
	}

	if s.audlog != nil {
		s.audlog.Log(audit.Entry{
			EventType: "invite_used",
			TenantID:  inviteTok.TenantID,
			IPAddress: s.remoteIP(r),
			Success:   true,
		})
	}

	const signupMsg = "Wenn die E-Mail-Adresse verfügbar ist, wurde eine Bestätigungs-E-Mail gesendet."

	u, err := s.users.CreateInactive(userstore.CreateUserRequest{
		Username: body.Username,
		Email:    body.Email,
		Password: body.Password,
		Role:     userstore.RoleUser,
		TenantID: inviteTok.TenantID,
	})
	if err != nil {
		// SEC: token already consumed — response is identical to success to prevent
		// enumeration of whether the email address already existed.
		go func() {
			html := mailer.AlreadyRegisteredHTML(s.fqdn)
			txt := mailer.AlreadyRegisteredText(s.fqdn)
			_ = s.mailer.Send(body.Email, "archivmail – Registrierungsversuch", html, txt)
		}()
		writeJSON(w, http.StatusOK, map[string]string{"message": signupMsg})
		return
	}

	// Generate verification token.
	uid := u.ID
	token, err := s.tokenStore.Create(r.Context(), tokenstore.TypeVerify, &uid, nil, 24*time.Hour)
	if err != nil {
		// Invite is consumed and user exists but has no verify token.
		// Log the error; admin can delete and re-invite the user.
		s.logger.Error("signup: failed to create verify token", "user_id", uid, "err", err)
		writeError(w, http.StatusInternalServerError, "token error")
		return
	}

	// Send verification email.
	go func() {
		html := mailer.VerifyEmailHTML(s.fqdn, token, u.Username)
		txt := mailer.VerifyEmailText(s.fqdn, token, u.Username)
		_ = s.mailer.Send(u.Email, "archivmail – E-Mail bestätigen", html, txt)
	}()

	if s.audlog != nil {
		s.audlog.Log(audit.Entry{
			EventType: "signup",
			Username:  u.Username,
			TenantID:  u.TenantID,
			IPAddress: s.remoteIP(r),
			Success:   true,
		})
	}

	writeJSON(w, http.StatusOK, map[string]string{"message": signupMsg})
}

// handleVerifyEmail activates an account via a verification token.
// GET /api/auth/verify?token=...  (PROJ-28)
func (s *Server) handleVerifyEmail(w http.ResponseWriter, r *http.Request) {
	plain := r.URL.Query().Get("token")
	if plain == "" {
		writeError(w, http.StatusBadRequest, "token required")
		return
	}

	tok, err := s.tokenStore.Use(r.Context(), tokenstore.TypeVerify, plain)
	if err != nil {
		writeError(w, http.StatusBadRequest, "ungültiger oder abgelaufener Link")
		return
	}

	if tok.UserID == nil {
		writeError(w, http.StatusBadRequest, "invalid token")
		return
	}

	if err := s.users.Activate(r.Context(), *tok.UserID); err != nil {
		writeError(w, http.StatusInternalServerError, err.Error())
		return
	}

	if s.audlog != nil {
		s.audlog.Log(audit.Entry{
			EventType: "email_verified",
			IPAddress: s.remoteIP(r),
			Success:   true,
		})
	}

	writeJSON(w, http.StatusOK, map[string]string{"message": "E-Mail-Adresse bestätigt. Du kannst dich jetzt anmelden."})
}

// handleForgotPassword sends a password-reset email.
// POST /api/auth/forgot-password  (PROJ-28)
func (s *Server) handleForgotPassword(w http.ResponseWriter, r *http.Request) {
	// Always respond with same message to avoid info-leakage
	const msg = "Falls ein Account mit dieser E-Mail existiert, wurde ein Reset-Link gesendet."

	if !s.mailer.IsConfigured() {
		writeError(w, http.StatusServiceUnavailable, "E-Mail-Versand nicht konfiguriert")
		return
	}

	var body struct {
		Email string `json:"email"`
	}
	if err := json.NewDecoder(r.Body).Decode(&body); err != nil || body.Email == "" {
		writeJSON(w, http.StatusOK, map[string]string{"message": msg})
		return
	}

	u, err := s.users.GetByEmail(r.Context(), body.Email)
	if err != nil || u == nil {
		writeJSON(w, http.StatusOK, map[string]string{"message": msg})
		return
	}

	// LDAP users cannot reset password here
	if u.Source != "local" {
		writeJSON(w, http.StatusOK, map[string]string{"message": msg})
		return
	}

	uid := u.ID
	token, err := s.tokenStore.Create(r.Context(), tokenstore.TypeReset, &uid, nil, 1*time.Hour)
	if err != nil {
		writeJSON(w, http.StatusOK, map[string]string{"message": msg})
		return
	}

	go func() {
		html := mailer.ResetPasswordHTML(s.fqdn, token, u.Username)
		txt := mailer.ResetPasswordText(s.fqdn, token, u.Username)
		_ = s.mailer.Send(u.Email, "archivmail – Passwort zurücksetzen", html, txt)
	}()

	if s.audlog != nil {
		s.audlog.Log(audit.Entry{
			EventType: "password_reset_requested",
			Username:  u.Username,
			TenantID:  u.TenantID,
			IPAddress: s.remoteIP(r),
			Success:   true,
		})
	}

	writeJSON(w, http.StatusOK, map[string]string{"message": msg})
}

// handleResetPassword sets a new password via a reset token.
// POST /api/auth/reset-password  (PROJ-28)
func (s *Server) handleResetPassword(w http.ResponseWriter, r *http.Request) {
	var body struct {
		Token    string `json:"token"`
		Password string `json:"password"`
	}
	if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
		writeError(w, http.StatusBadRequest, "invalid body")
		return
	}
	if body.Token == "" || body.Password == "" {
		writeError(w, http.StatusBadRequest, "token and password required")
		return
	}
	if len(body.Password) < 8 {
		writeError(w, http.StatusBadRequest, "password must be at least 8 characters")
		return
	}

	tok, err := s.tokenStore.Use(r.Context(), tokenstore.TypeReset, body.Token)
	if err != nil {
		writeError(w, http.StatusBadRequest, "ungültiger oder abgelaufener Link")
		return
	}

	if tok.UserID == nil {
		writeError(w, http.StatusBadRequest, "invalid token")
		return
	}

	if err := s.users.SetPassword(r.Context(), *tok.UserID, body.Password); err != nil {
		writeError(w, http.StatusInternalServerError, err.Error())
		return
	}

	if s.audlog != nil {
		s.audlog.Log(audit.Entry{
			EventType: "password_reset_done",
			IPAddress: s.remoteIP(r),
			Success:   true,
		})
	}

	writeJSON(w, http.StatusOK, map[string]string{"message": "Passwort erfolgreich zurückgesetzt."})
}
