package controllers

import (
	configs "ares/config"
	database "ares/database/config"
	"ares/database/models"
	"ares/middlewares"
	utils "ares/pkg/utis"
	"crypto/sha256"
	"ares/services"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	"github.com/go-playground/validator/v10"
	"github.com/gofiber/fiber/v3"
	"golang.org/x/crypto/bcrypt"
	"gorm.io/gorm"
)

var validate = validator.New()

type RegisterRequest struct {
	UserName  string `json:"username" validate:"required,min=3"`
	Email     string `json:"email" validate:"required,email"`
	Password  string `json:"password" validate:"required,min=6"`
	FirstName string `json:"first_name" validate:"required"`
	LastName  string `json:"last_name" validate:"required"`
}

type LoginRequest struct {
	Email    string `json:"email" validate:"required,email"`
	Password string `json:"password" validate:"required"`
}

type RefreshRequest struct {
	RefreshToken string `json:"refresh_token" validate:"required"`
}

type ResendVerificationRequest struct {
	Email string `json:"email" validate:"required,email"`
}

// UpdateUserRequest represents allowed fields for updating a user
type UpdateUserRequest struct {
	UserName      string `json:"username,omitempty" example:"jdoe"`
	Email         string `json:"email,omitempty" example:"jdoe@example.com"`
	IsAdmin       *bool  `json:"is_admin,omitempty" example:"false"`
	Password      string `json:"password,omitempty" example:"#secret"`
	FirstName     string `json:"first_name,omitempty" example:"John"`
	LastName      string `json:"last_name,omitempty" example:"Doe"`
	AvatarURL     string `json:"avatar_url,omitempty" example:"/uploads/avatar.jpg"`
	EmailVerified *bool  `json:"email_verified,omitempty" example:"true"`
	// Accept avatar file via multipart/form-data with field name "avatar" when using form upload
}

func GetUser(c fiber.Ctx) error {
	return c.Status(fiber.StatusOK).SendString("Get User")
}

func AdminListUsers(c fiber.Ctx) error {
	if database.DB == nil {
		return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "database is not configured"})
	}

	var users []models.User
	if err := database.DB.Preload("Profile").Order("id DESC").Find(&users).Error; err != nil {
		return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"})
	}

	return c.JSON(fiber.Map{
		"count": len(users),
		"users": users,
	})
}

func AdminListDeletedUsers(c fiber.Ctx) error {
	if database.DB == nil {
		return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "database is not configured"})
	}

	var users []models.User
	if err := database.DB.Unscoped().
		Preload("Profile").
		Where("deleted_at IS NOT NULL").
		Order("deleted_at DESC").
		Find(&users).Error; err != nil {
		return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"})
	}

	return c.JSON(fiber.Map{
		"count": len(users),
		"users": users,
	})
}

func GetUserOne(c fiber.Ctx) error {
	return c.Status(fiber.StatusOK).SendString("Get User One")
}

func UpdateUser(c fiber.Ctx) error {
	if database.DB == nil {
		return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "database is not configured"})
	}

	id, err := strconv.ParseUint(c.Params("id"), 10, 64)
	if err != nil || id == 0 {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid user id"})
	}

	var user models.User
	if err := database.DB.Preload("Profile").First(&user, id).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "user not found"})
		}
		return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"})
	}

	// Parse incoming JSON or multipart/form-data into map to allow partial updates including false values
	var payload map[string]interface{}

	// Prefer detecting multipart by trying to read the multipart form first
	if mf, err := c.MultipartForm(); err == nil && mf != nil {
		payload = map[string]interface{}{}
		// form values
		for k, vals := range mf.Value {
			if len(vals) > 0 {
				payload[k] = vals[0]
			}
		}

		// handle avatar file if present
		if files, ok := mf.File["avatar"]; ok && len(files) > 0 {
			file := files[0]
			if _, err := os.Stat("./uploads/avatars"); os.IsNotExist(err) {
				os.MkdirAll("./uploads/avatars", 0755)
			}
			filename := fmt.Sprintf("%d_%s", time.Now().Unix(), file.Filename)
			filePath := filepath.Join("./uploads/avatars", filename)
			if err := c.SaveFile(file, filePath); err != nil {
				if configs.Logger != nil {
					configs.Logger.Sugar().Errorf("failed to save avatar: %v", err)
				}
			} else {
				payload["avatar_url"] = "/uploads/avatars/" + filename
			}
		}
	} else {
		// fallback to JSON body
		if err := json.Unmarshal(c.Body(), &payload); err != nil {
			return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
		}
	}

	// Prepare updates for user table
	userUpdates := map[string]interface{}{}
	if v, ok := payload["username"].(string); ok {
		userUpdates["user_name"] = v
		userUpdates["user_name"] = v
		user.UserName = v
	}
	if v, ok := payload["email"].(string); ok {
		userUpdates["email"] = v
		user.Email = v
	}
	if v, ok := payload["is_admin"]; ok {
		// handle bool or string representations
		switch val := v.(type) {
		case bool:
			userUpdates["is_admin"] = val
			user.IsAdmin = &val
		case string:
			if parsed, err := strconv.ParseBool(val); err == nil {
				userUpdates["is_admin"] = parsed
				user.IsAdmin = &parsed
			}
		}
	}
	// Handle email_verified explicitly (bool or string)
	if v, ok := payload["email_verified"]; ok {
		switch val := v.(type) {
		case bool:
			userUpdates["email_verified"] = val
			now := time.Now()
			if val {
				userUpdates["email_verified_at"] = now
				user.EmailVerified = &val
				user.EmailVerifiedAt = &now
			} else {
				userUpdates["email_verified_at"] = nil
				user.EmailVerified = &val
				user.EmailVerifiedAt = nil
			}
		case string:
			if parsed, err := strconv.ParseBool(val); err == nil {
				userUpdates["email_verified"] = parsed
				now := time.Now()
				if parsed {
					userUpdates["email_verified_at"] = now
					user.EmailVerified = &parsed
					user.EmailVerifiedAt = &now
				} else {
					userUpdates["email_verified_at"] = nil
					user.EmailVerified = &parsed
					user.EmailVerifiedAt = nil
				}
			}
		}
	}
	if v, ok := payload["password"].(string); ok && v != "" {
		hashed, err := bcrypt.GenerateFromPassword([]byte(v), bcrypt.DefaultCost)
		if err != nil {
			return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "could not hash password"})
		}
		userUpdates["password"] = string(hashed)
		user.Password = string(hashed)
	}

	// Apply user updates if any
	if len(userUpdates) > 0 {
		if err := database.DB.Model(&user).Updates(userUpdates).Error; err != nil {
			return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "user could not be updated"})
		}
	}

	// Handle profile updates (first_name, last_name, avatar_url)
	profileUpdates := map[string]interface{}{}
	if v, ok := payload["first_name"].(string); ok {
		profileUpdates["first_name"] = v
	}
	if v, ok := payload["last_name"].(string); ok {
		profileUpdates["last_name"] = v
	}
	if v, ok := payload["avatar_url"].(string); ok {
		profileUpdates["avatar_url"] = v
	}

	if len(profileUpdates) > 0 {
		// Profile may be stored as slice; update first profile if exists else create
		var profile models.Profile
		if len(user.Profile) > 0 {
			profile = user.Profile[0]
			if err := database.DB.Model(&profile).Updates(profileUpdates).Error; err != nil {
				return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "profile could not be updated"})
			}
		} else {
			profile = models.Profile{
				UserID: uint64(user.ID),
			}
			if v, ok := profileUpdates["first_name"].(string); ok {
				profile.FirstName = v
			}
			if v, ok := profileUpdates["last_name"].(string); ok {
				profile.LastName = v
			}
			if v, ok := profileUpdates["avatar_url"].(string); ok {
				profile.AvatarURL = v
			}
			if err := database.DB.Create(&profile).Error; err != nil {
				return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "profile could not be created"})
			}
		}
	}

	// Reload user with profile
	if err := database.DB.Preload("Profile").First(&user, id).Error; err != nil {
		return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"})
	}

	return c.JSON(fiber.Map{"message": "user updated", "user": user})
}

func DeleteUser(c fiber.Ctx) error {
	if database.DB == nil {
		return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "database is not configured"})
	}

	id, err := strconv.ParseUint(c.Params("id"), 10, 64)
	if err != nil || id == 0 {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid user id"})
	}

	var user models.User
	if err := database.DB.First(&user, id).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "user not found"})
		}
		return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"})
	}

	if err := database.DB.Delete(&user).Error; err != nil {
		return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "user could not be deleted"})
	}

	return c.JSON(fiber.Map{
		"message": "user soft-deleted successfully",
		"user_id": id,
	})
}

func HardDeleteUser(c fiber.Ctx) error {
	if database.DB == nil {
		return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "database is not configured"})
	}

	id, err := strconv.ParseUint(c.Params("id"), 10, 64)
	if err != nil || id == 0 {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid user id"})
	}

	var user models.User
	if err := database.DB.Unscoped().First(&user, id).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "user not found"})
		}
		return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"})
	}

	err = database.DB.Transaction(func(tx *gorm.DB) error {
		if err := tx.Unscoped().Where("user_id = ?", id).Delete(&models.Profile{}).Error; err != nil {
			return err
		}
		if err := tx.Unscoped().Where("user_id = ?", id).Delete(&models.SocialAccount{}).Error; err != nil {
			return err
		}
		return tx.Unscoped().Delete(&models.User{}, id).Error
	})
	if err != nil {
		return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "user hard-delete failed"})
	}

	return c.JSON(fiber.Map{
		"message": "user permanently deleted",
		"user_id": id,
	})
}

func RestoreUser(c fiber.Ctx) error {
	if database.DB == nil {
		return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "database is not configured"})
	}

	id, err := strconv.ParseUint(c.Params("id"), 10, 64)
	if err != nil || id == 0 {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid user id"})
	}

	var user models.User
	if err := database.DB.Unscoped().First(&user, id).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "user not found"})
		}
		return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"})
	}

	if !user.DeletedAt.Valid {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "user is not soft-deleted"})
	}

	if err := database.DB.Unscoped().Model(&models.User{}).Where("id = ?", id).Update("deleted_at", nil).Error; err != nil {
		return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "user could not be restored"})
	}

	return c.JSON(fiber.Map{
		"message": "user restored successfully",
		"user_id": id,
	})
}

// Register godoc
// @Summary Register user
// @Tags Auth
// @Accept json
// @Produce json
// @Param request body RegisterRequest true "Register payload"
// @Success 201 {object} map[string]interface{}
// @Failure 400 {object} map[string]string
// @Failure 409 {object} map[string]string
// @Router /api/v1/auth/register [post]
func Register(c fiber.Ctx) error {
	if database.DB == nil {
		return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "database is not configured"})
	}

	var req RegisterRequest
	if err := c.Bind().JSON(&req); err != nil {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
	}
	if err := validate.Struct(req); err != nil {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
	}

	hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
	if err != nil {
		return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "password could not be hashed"})
	}

	verifyToken, err := utils.GenerateSecureToken(32)
	if err != nil {
		return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "verify token could not be generated"})
	}

	user := models.User{
		UserName:         req.UserName,
		Email:            req.Email,
		Password:         string(hashedPassword),
		EmailVerifyToken: verifyToken,
	}

	if err := database.DB.Create(&user).Error; err != nil {
		return c.Status(http.StatusConflict).JSON(fiber.Map{"error": "email already exists"})
	}

	profile := models.Profile{
		UserID:    uint64(user.ID),
		FirstName: req.FirstName,
		LastName:  req.LastName,
	}
	if err := database.DB.Create(&profile).Error; err != nil {
		return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "profile could not be created"})
	}

	appURL := strings.TrimRight(configs.AppConfig.AppURL, "/")
	verifyURL := fmt.Sprintf("%s/api/v1/auth/verify-email?token=%s", appURL, url.QueryEscape(verifyToken))

	emailService := services.NewEmailService()
	err = emailService.SendVerificationEmail(user.Email, profile.FirstName, verifyURL)
	if err != nil {
		return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "verification email could not be sent"})
	}

	return c.Status(http.StatusCreated).JSON(fiber.Map{
		"message": "registration successful, please verify your email before login",
		"user": fiber.Map{
			"id":             user.ID,
			"username":       user.UserName,
			"email":          user.Email,
			"is_admin":       boolPtrValue(user.IsAdmin),
			"email_verified": false,
			"first_name":     profile.FirstName,
			"last_name":      profile.LastName,
		},
	})
}

// Login godoc
// @Summary Login user
// @Tags Auth
// @Accept json
// @Produce json
// @Param request body LoginRequest true "Login payload"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]string
// @Failure 401 {object} map[string]string
// @Router /api/v1/auth/login [post]
func Login(c fiber.Ctx) error {
	if database.DB == nil {
		return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "database is not configured"})
	}

	var req LoginRequest
	if err := c.Bind().JSON(&req); err != nil {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
	}
	if err := validate.Struct(req); err != nil {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
	}

	var user models.User
	if err := database.DB.Preload("Profile").Where("email = ?", req.Email).First(&user).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return c.Status(http.StatusUnauthorized).JSON(fiber.Map{"error": "invalid email or password"})
		}
		return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"})
	}

	if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil {
		return c.Status(http.StatusUnauthorized).JSON(fiber.Map{"error": "invalid email or password"})
	}

	if !user.IsEmailVerified() {
		return c.Status(http.StatusForbidden).JSON(fiber.Map{"error": "please verify your email before login"})
	}

	firstName, lastName := extractProfileName(user.Profile)
	jwtService := services.NewJWTService()
	accessToken, refreshToken, err := jwtService.GenerateTokenPair(
		user.ID,
		user.Email,
		boolPtrValue(user.IsAdmin),
		firstName,
		lastName,
	)
	if err != nil {
		return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "tokens could not be generated"})
	}

	// Persist refresh token server-side for rotation & revoke
	refreshClaims, err := jwtService.ValidateToken(refreshToken)
	if err != nil || refreshClaims.TokenType != services.TokenTypeRefresh {
		if configs.Logger != nil {
			configs.Logger.Sugar().Errorf("failed to validate issued refresh token for user=%d: %v", user.ID, err)
		}
	} else {
		// Revoke any existing refresh tokens for this user (single-device semantics)
		if err := database.DB.Model(&models.RefreshToken{}).
			Where("user_id = ?", user.ID).
			Update("revoked", true).Error; err != nil {
			if configs.Logger != nil {
				configs.Logger.Sugar().Errorf("failed to revoke existing refresh tokens for user=%d: %v", user.ID, err)
			}
		}

		expiresAt := time.Now().Add(time.Duration(configs.AppConfig.RefreshTokenExpireDays) * 24 * time.Hour)
		if refreshClaims.ExpiresAt != nil {
			expiresAt = refreshClaims.ExpiresAt.Time
		}

		rt := models.RefreshToken{
			UserID:    user.ID,
			TokenID:   refreshClaims.ID,
			TokenHash: sha256Hex(refreshToken),
			TokenFingerprint: tokenFingerprint(refreshToken),
			ExpiresAt: expiresAt.UTC(),
			Revoked:   false,
			UserAgent: c.Get("User-Agent"),
			IP:        c.IP(),
		}
		if err := database.DB.Create(&rt).Error; err != nil {
			if configs.Logger != nil {
				configs.Logger.Sugar().Errorf("failed to persist refresh token for user=%d: %v", user.ID, err)
			}
			return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "refresh token could not be persisted"})
		}
		if configs.Logger != nil {
			configs.Logger.Sugar().Infof("refresh token persisted user=%d token_id=%s fingerprint=%s", user.ID, rt.TokenID, rt.TokenFingerprint)
		}
	}

	return c.JSON(fiber.Map{
		"user": fiber.Map{
			"id":         user.ID,
			"username":   user.UserName,
			"email":      user.Email,
			"is_admin":   boolPtrValue(user.IsAdmin),
			"first_name": firstName,
			"last_name":  lastName,
		},
		"access_token":  accessToken,
		"refresh_token": refreshToken,
	})
}

// RefreshToken godoc
// @Summary Refresh access token
// @Tags Auth
// @Accept json
// @Produce json
// @Param request body RefreshRequest true "Refresh payload"
// @Success 200 {object} map[string]string
// @Failure 400 {object} map[string]string
// @Failure 401 {object} map[string]string
// @Router /api/v1/auth/refresh [post]
func RefreshToken(c fiber.Ctx) error {
	if database.DB == nil {
		return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "database is not configured"})
	}

	var req RefreshRequest
	if err := c.Bind().JSON(&req); err != nil {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
	}
	if err := validate.Struct(req); err != nil {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
	}

	jwtService := services.NewJWTService()
	claims, err := jwtService.ValidateToken(req.RefreshToken)
	if err != nil || claims.TokenType != services.TokenTypeRefresh {
		return c.Status(http.StatusUnauthorized).JSON(fiber.Map{"error": "invalid refresh token"})
	}

	// Look up refresh token server-side to enforce rotation and revoke state
	var stored models.RefreshToken
	if err := database.DB.Where("token_id = ? AND user_id = ?", claims.ID, claims.UserID).First(&stored).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return c.Status(http.StatusUnauthorized).JSON(fiber.Map{"error": "invalid or expired refresh token"})
		}
		return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "refresh token lookup failed"})
	}

	now := time.Now().UTC()
	if stored.Revoked || stored.ExpiresAt.Before(now) {
		return c.Status(http.StatusUnauthorized).JSON(fiber.Map{"error": "invalid or expired refresh token"})
	}
	// Extra safety: if we have a stored hash, require it to match.
	if stored.TokenHash != "" && stored.TokenHash != sha256Hex(req.RefreshToken) {
		return c.Status(http.StatusUnauthorized).JSON(fiber.Map{"error": "invalid refresh token"})
	}

	// Reuse detection: if this token was already rotated to a new one, treat as suspicious
	if stored.ReplacedByTokenID != "" {
		if configs.Logger != nil {
			configs.Logger.Sugar().Warnf("refresh token reuse detected for user=%d token_id=%s", claims.UserID, claims.ID)
		}
		// Revoke all refresh tokens for this user to force re-login
		if err := database.DB.Model(&models.RefreshToken{}).
			Where("user_id = ?", claims.UserID).
			Update("revoked", true).Error; err != nil {
			if configs.Logger != nil {
				configs.Logger.Sugar().Errorf("failed to revoke refresh tokens after reuse for user=%d: %v", claims.UserID, err)
			}
		}
		return c.Status(http.StatusUnauthorized).JSON(fiber.Map{"error": "refresh token has been reused; please login again"})
	}

	var user models.User
	if err := database.DB.Preload("Profile").First(&user, claims.UserID).Error; err != nil {
		return c.Status(http.StatusUnauthorized).JSON(fiber.Map{"error": "user not found"})
	}

	firstName, lastName := extractProfileName(user.Profile)
	accessToken, refreshToken, err := jwtService.GenerateTokenPair(
		user.ID,
		user.Email,
		boolPtrValue(user.IsAdmin),
		firstName,
		lastName,
	)
	if err != nil {
		return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "tokens could not be generated"})
	}

	// Persist new refresh token and rotate old one
	newClaims, err := jwtService.ValidateToken(refreshToken)
	if err != nil || newClaims.TokenType != services.TokenTypeRefresh {
		if configs.Logger != nil {
			configs.Logger.Sugar().Errorf("failed to validate rotated refresh token for user=%d: %v", user.ID, err)
		}
		return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "refresh token rotation failed"})
	}

	expiresAt := now.Add(time.Duration(configs.AppConfig.RefreshTokenExpireDays) * 24 * time.Hour)
	if newClaims.ExpiresAt != nil {
		expiresAt = newClaims.ExpiresAt.Time
	}

	if err := database.DB.Transaction(func(tx *gorm.DB) error {
		if err := tx.Model(&stored).Updates(map[string]interface{}{
			"revoked":             true,
			"replaced_by_token_id": newClaims.ID,
		}).Error; err != nil {
			return err
		}

		rt := models.RefreshToken{
			UserID:    user.ID,
			TokenID:   newClaims.ID,
			TokenHash: sha256Hex(refreshToken),
			TokenFingerprint: tokenFingerprint(refreshToken),
			ExpiresAt: expiresAt.UTC(),
			Revoked:   false,
			UserAgent: c.Get("User-Agent"),
			IP:        c.IP(),
		}
		if err := tx.Create(&rt).Error; err != nil {
			return err
		}
		return nil
	}); err != nil {
		if configs.Logger != nil {
			configs.Logger.Sugar().Errorf("failed to rotate refresh token for user=%d: %v", user.ID, err)
		}
		return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "refresh token rotation failed"})
	}

	return c.JSON(fiber.Map{
		"access_token":  accessToken,
		"refresh_token": refreshToken,
	})
}

func sha256Hex(s string) string {
	sum := sha256.Sum256([]byte(s))
	return hex.EncodeToString(sum[:])
}

func tokenFingerprint(token string) string {
	token = strings.TrimSpace(token)
	if len(token) <= 10 {
		return "****"
	}
	return token[:6] + "..." + token[len(token)-4:]
}

// Logout godoc
// @Summary Logout user and revoke refresh tokens
// @Tags Auth
// @Accept json
// @Produce json
// @Param request body RefreshRequest true "Logout payload (refresh token)"
// @Success 200 {object} map[string]string
// @Failure 400 {object} map[string]string
// @Failure 401 {object} map[string]string
// @Router /api/v1/auth/logout [post]
func Logout(c fiber.Ctx) error {
	if database.DB == nil {
		return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "database is not configured"})
	}

	var req RefreshRequest
	if err := c.Bind().JSON(&req); err != nil {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
	}
	if err := validate.Struct(req); err != nil {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
	}

	jwtService := services.NewJWTService()
	claims, err := jwtService.ValidateToken(req.RefreshToken)
	if err != nil || claims.TokenType != services.TokenTypeRefresh {
		return c.Status(http.StatusUnauthorized).JSON(fiber.Map{"error": "invalid refresh token"})
	}

	// Revoke all refresh tokens for this user to enforce full logout
	if err := database.DB.Model(&models.RefreshToken{}).
		Where("user_id = ?", claims.UserID).
		Update("revoked", true).Error; err != nil {
		if configs.Logger != nil {
			configs.Logger.Sugar().Errorf("failed to revoke refresh tokens on logout for user=%d: %v", claims.UserID, err)
		}
		return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "logout failed"})
	}

	return c.JSON(fiber.Map{"message": "logged out successfully"})
}

// VerifyEmail godoc
// @Summary Verify email address with token
// @Tags Auth
// @Produce json
// @Param token query string true "Email verify token"
// @Success 200 {object} map[string]string
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Router /api/v1/auth/verify-email [get]
func VerifyEmail(c fiber.Ctx) error {
	if database.DB == nil {
		return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "database is not configured"})
	}

	token := strings.TrimSpace(c.Query("token"))
	if token == "" {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "token is required"})
	}

	var user models.User
	if err := database.DB.Where("email_verify_token = ?", token).First(&user).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "invalid or expired token"})
		}
		return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"})
	}

	now := time.Now()
	isVerified := true
	user.EmailVerified = &isVerified
	user.EmailVerifiedAt = &now
	user.EmailVerifyToken = ""

	if err := database.DB.Save(&user).Error; err != nil {
		return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "email verification could not be saved"})
	}

	return c.JSON(fiber.Map{"message": "email verified successfully"})
}

// ResendVerificationEmail godoc
// @Summary Resend verification email
// @Tags Auth
// @Accept json
// @Produce json
// @Param request body ResendVerificationRequest true "Resend verification payload"
// @Success 200 {object} map[string]string
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Router /api/v1/auth/resend-verification [post]
func ResendVerificationEmail(c fiber.Ctx) error {
	if database.DB == nil {
		return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "database is not configured"})
	}

	var req ResendVerificationRequest
	if err := c.Bind().JSON(&req); err != nil {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
	}
	if err := validate.Struct(req); err != nil {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
	}

	var user models.User
	if err := database.DB.Preload("Profile").Where("email = ?", req.Email).First(&user).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "user not found"})
		}
		return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"})
	}

	if user.IsEmailVerified() {
		return c.JSON(fiber.Map{"message": "email is already verified"})
	}

	verifyToken, err := utils.GenerateSecureToken(32)
	if err != nil {
		return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "verify token could not be generated"})
	}
	user.EmailVerifyToken = verifyToken
	if err := database.DB.Save(&user).Error; err != nil {
		return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "verification token could not be saved"})
	}

	firstName, _ := extractProfileName(user.Profile)
	appURL := strings.TrimRight(configs.AppConfig.AppURL, "/")
	verifyURL := fmt.Sprintf("%s/api/v1/auth/verify-email?token=%s", appURL, url.QueryEscape(verifyToken))

	emailService := services.NewEmailService()
	if err := emailService.SendVerificationEmail(user.Email, firstName, verifyURL); err != nil {
		return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "verification email could not be sent"})
	}

	return c.JSON(fiber.Map{"message": "verification email has been sent"})
}

// Me godoc
// @Summary Get current user from token
// @Tags Auth
// @Produce json
// @Security BearerAuth
// @Success 200 {object} map[string]interface{}
// @Failure 401 {object} map[string]string
// @Router /api/v1/auth/me [get]
func Me(c fiber.Ctx) error {
	claims, ok := middlewares.GetAuthClaims(c)
	if !ok {
		return c.Status(http.StatusUnauthorized).JSON(fiber.Map{"error": "unauthorized"})
	}

	return c.JSON(fiber.Map{
		"user": fiber.Map{
			"id":         claims.UserID,
			"email":      claims.Email,
			"is_admin":   claims.IsAdmin,
			"first_name": claims.FirstName,
			"last_name":  claims.LastName,
		},
	})
}

func AdminOnlyExample(c fiber.Ctx) error {
	claims, ok := middlewares.GetAuthClaims(c)
	if !ok {
		return c.Status(http.StatusUnauthorized).JSON(fiber.Map{"error": "unauthorized"})
	}

	return c.JSON(fiber.Map{
		"message": "only admins can access this endpoint",
		"user":    claims.Email,
	})
}

func UserOnlyExample(c fiber.Ctx) error {
	claims, ok := middlewares.GetAuthClaims(c)
	if !ok {
		return c.Status(http.StatusUnauthorized).JSON(fiber.Map{"error": "unauthorized"})
	}

	return c.JSON(fiber.Map{
		"message": "only normal users can access this endpoint",
		"user":    claims.Email,
	})
}

func GoogleAuth(c fiber.Ctx) error {
	if configs.AppConfig.GoogleClientID == "" {
		return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "google oauth is not configured"})
	}

	stateToken, err := utils.GenerateSecureToken(16)
	if err != nil {
		return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "state token could not be generated"})
	}

	authURL := "https://accounts.google.com/o/oauth2/v2/auth?" + url.Values{
		"client_id":     []string{configs.AppConfig.GoogleClientID},
		"redirect_uri":  []string{configs.AppConfig.GoogleRedirectURL},
		"response_type": []string{"code"},
		"scope":         []string{"openid email profile"},
		"state":         []string{stateToken},
	}.Encode()

	return c.JSON(fiber.Map{"provider": "google", "auth_url": authURL, "state": stateToken})
}

func GoogleAuthCallback(c fiber.Ctx) error {
	code := c.Query("code")
	if code == "" {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "google callback code is missing"})
	}

	// OAuth token exchange is intentionally left simple for now.
	return c.JSON(fiber.Map{
		"provider": "google",
		"message":  "google callback infrastructure is ready, token exchange can be added next",
		"code":     code,
		"state":    c.Query("state"),
	})
}

func GithubAuth(c fiber.Ctx) error {
	if configs.AppConfig.GithubClientID == "" {
		return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "github oauth is not configured"})
	}

	stateToken, err := utils.GenerateSecureToken(16)
	if err != nil {
		return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "state token could not be generated"})
	}

	authURL := "https://github.com/login/oauth/authorize?" + url.Values{
		"client_id":    []string{configs.AppConfig.GithubClientID},
		"redirect_uri": []string{configs.AppConfig.GithubRedirectURL},
		"scope":        []string{"read:user user:email"},
		"state":        []string{stateToken},
	}.Encode()

	return c.JSON(fiber.Map{"provider": "github", "auth_url": authURL, "state": stateToken})
}

func GithubAuthCallback(c fiber.Ctx) error {
	code := c.Query("code")
	if code == "" {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "github callback code is missing"})
	}

	// OAuth token exchange is intentionally left simple for now.
	return c.JSON(fiber.Map{
		"provider": "github",
		"message":  "github callback infrastructure is ready, token exchange can be added next",
		"code":     code,
		"state":    c.Query("state"),
	})
}

func extractProfileName(profiles []models.Profile) (string, string) {
	if len(profiles) == 0 {
		return "", ""
	}
	return profiles[0].FirstName, profiles[0].LastName
}

func boolPtrValue(v *bool) bool {
	if v == nil {
		return false
	}
	return *v
}
