package accountapiimp

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

	"github.com/tech/sendico/pkg/api/http/response"
	"github.com/tech/sendico/pkg/merrors"
	"github.com/tech/sendico/pkg/model"
	"github.com/tech/sendico/pkg/mutil/mzap"
	"github.com/tech/sendico/server/interface/api/srequest"
	"github.com/tech/sendico/server/interface/api/sresponse"
	mutil "github.com/tech/sendico/server/internal/mutil/param"
	"go.uber.org/zap"
)

func (a *AccountAPI) checkPassword(_ *http.Request, account *model.Account, accessToken *sresponse.TokenData) http.HandlerFunc {
	return sresponse.Account(a.logger, account, accessToken)
}

func (a *AccountAPI) changePassword(r *http.Request, user *model.Account, token *sresponse.TokenData) http.HandlerFunc {
	// TODO: add rate check
	var pcr srequest.ChangePassword
	if err := json.NewDecoder(r.Body).Decode(&pcr); err != nil {
		a.logger.Warn("Failed to decode password change request", zap.Error(err), mzap.StorableRef(user))
		return response.BadPayload(a.logger, a.Name(), err)
	}

	if err := a.accService.ValidatePassword(pcr.New, &pcr.Old); err != nil {
		a.logger.Debug("Password validation failed", zap.Error(err), mzap.StorableRef(user))
		return sresponse.BadRPassword(a.logger, a.Name(), err)
	}

	ctx := r.Context()
	if !user.MatchPassword(pcr.Old) {
		a.logger.Info("Old password does not match", mzap.StorableRef(user))
		return a.reportUnauthorized("old password does not match")
	}
	user.Password = pcr.New
	if err := user.HashPassword(); err != nil {
		a.logger.Warn("Failed to hash new password", zap.Error(err), mzap.StorableRef(user))
		return response.Internal(a.logger, a.Name(), err)
	}
	if err := a.db.Update(ctx, user); err != nil {
		a.logger.Warn("Failed to save account", zap.Error(err), mzap.StorableRef(user))
		return response.Internal(a.logger, a.Name(), err)
	}

	if err := a.rtdb.RevokeAll(ctx, *user.GetID(), pcr.DeviceID); err != nil {
		a.logger.Warn("Failed to revoke refresh tokens",
			zap.Error(err), mzap.StorableRef(user), zap.String("device_id", pcr.DeviceID))
	}

	return sresponse.Account(a.logger, user, token)
}

func (a *AccountAPI) forgotPassword(r *http.Request) http.HandlerFunc {
	var req srequest.ForgotPassword
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		a.logger.Warn("Failed to decode password change request", zap.Error(err))
		return response.BadPayload(a.logger, a.Name(), err)
	}
	if req.Login == "" {
		a.logger.Debug("Email is missing in the request")
		return a.reportEmailMissing()
	}

	// Always use the lower case email address
	req.Login = strings.ToLower(req.Login)

	// Get user
	ctx := r.Context()
	user, err := a.db.GetByEmail(ctx, req.Login)
	if (errors.Is(err, merrors.ErrNoData)) || (user == nil) {
		a.logger.Debug("User not found while recovering password", zap.Error(err), zap.String("email", req.Login))
		return a.reportNoEmailRegistered()
	}
	if err != nil {
		a.logger.Warn("Failed to fetch user", zap.Error(err), zap.String("email", req.Login))
		return response.Auto(a.logger, a.Name(), err)
	}

	// Generate reset password token
	verificationToken, err := a.accService.ResetPassword(ctx, user)
	if err != nil {
		a.logger.Warn("Failed to generate reset password token", zap.Error(err), mzap.StorableRef(user))
		return response.Auto(a.logger, a.Name(), err)
	}

	// Send reset password email
	if err = a.sendPasswordResetEmail(user, verificationToken); err != nil {
		a.logger.Warn("Failed to send reset password email", zap.Error(err), mzap.StorableRef(user))
		return response.Auto(a.logger, a.Name(), err)
	}

	a.logger.Info("Reset password email sent successfully", zap.String("email", user.Login))
	return response.Success(a.logger)
}

func (a *AccountAPI) resetPassword(r *http.Request) http.HandlerFunc {
	ctx := r.Context()

	// Get account reference and token from URL parameters using parameter helpers
	accountRef, err := a.aph.GetRef(r)
	if err != nil {
		a.logger.Warn("Failed to get account reference", zap.Error(err), mutil.PLog(a.aph, r))
		return response.BadReference(a.logger, a.Name(), a.aph.Name(), a.aph.GetID(r), err)
	}

	token := a.tph.GetID(r)
	if token == "" {
		a.logger.Warn("Missing token in reset password request")
		return response.BadRequest(a.logger, a.Name(), "missing_parameters", "token is required")
	}

	// Get user from database
	var user model.Account
	err = a.db.Get(ctx, accountRef, &user)
	if errors.Is(err, merrors.ErrNoData) {
		a.logger.Info("User not found for password reset", mzap.ObjRef("account_ref", accountRef))
		return response.NotFound(a.logger, a.Name(), "User not found")
	}
	if err != nil {
		a.logger.Warn("Failed to get user for password reset", zap.Error(err), mzap.ObjRef("account_ref", accountRef))
		return response.Auto(a.logger, a.Name(), err)
	}

	t, err := a.vdb.Consume(ctx, accountRef, model.PurposePasswordReset, token)
	if err != nil {
		a.logger.Warn("Failed to consume password reset token", zap.Error(err), zap.String("token", token))
		return a.mapTokenErrorToResponse(err)
	}

	if t.Purpose != model.PurposePasswordReset {
		a.logger.Warn("Invalid token purpose for password reset", zap.String("expected", string(model.PurposePasswordReset)), zap.String("actual", string(t.Purpose)))
		return response.DataConflict(a.logger, a.Name(), "Invalid token purpose")
	}

	if t.AccountRef != accountRef {
		a.logger.Warn("Token account reference does not match request account reference", mzap.ObjRef("token_account_ref", t.AccountRef), mzap.ObjRef("request_account_ref", accountRef))
		return response.DataConflict(a.logger, a.Name(), "Token does not match account")
	}

	// Parse new password from request body
	var req srequest.ResetPassword
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		a.logger.Warn("Failed to decode reset password request", zap.Error(err))
		return response.BadPayload(a.logger, a.Name(), err)
	}

	// Validate new password
	if err := a.accService.ValidatePassword(req.Password, nil); err != nil {
		a.logger.Debug("Password validation failed", zap.Error(err), mzap.StorableRef(&user))
		return sresponse.BadRPassword(a.logger, a.Name(), err)
	}

	// Execute password reset in transaction to ensure atomicity
	if _, err := a.tf.CreateTransaction().Execute(ctx, func(ctx context.Context) (any, error) {
		return a.resetPasswordTransactionBody(ctx, &user, req.Password)
	}); err != nil {
		a.logger.Warn("Failed to execute password reset transaction", zap.Error(err), mzap.StorableRef(&user))
		return response.Auto(a.logger, a.Name(), err)
	}

	a.logger.Info("Password reset successful", mzap.StorableRef(&user))
	return response.Success(a.logger)
}

// resetPasswordTransactionBody contains the transaction logic for password reset
func (a *AccountAPI) resetPasswordTransactionBody(ctx context.Context, user *model.Account, newPassword string) (any, error) {
	// Update user with new password and clear reset token
	user.Password = newPassword

	// Hash the new password
	if err := user.HashPassword(); err != nil {
		a.logger.Warn("Failed to hash new password", zap.Error(err), mzap.StorableRef(user))
		return nil, err
	}

	// Save the updated user
	if err := a.db.Update(ctx, user); err != nil {
		a.logger.Warn("Failed to save user with new password", zap.Error(err), mzap.StorableRef(user))
		return nil, err
	}

	// Revoke all refresh tokens for this user (force re-login)
	if err := a.rtdb.RevokeAll(ctx, user.ID, ""); err != nil {
		a.logger.Warn("Failed to revoke refresh tokens after password reset", zap.Error(err), mzap.StorableRef(user))
		// Don't fail the transaction if token revocation fails, but log it
	}

	return struct{}{}, nil
}
