package handlers

import (
	"errors"
	"net/http"
	"os"
	"strings"
	"time"

	"github.com/gin-gonic/gin"
	"golang.org/x/crypto/bcrypt"

	"sproutgate-backend/internal/auth"
	"sproutgate-backend/internal/clientgeo"
	"sproutgate-backend/internal/models"
)

func (h *Handler) Login(c *gin.Context) {
	var req loginRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"})
		return
	}
	req.Account = strings.TrimSpace(req.Account)
	if req.Account == "" || req.Password == "" {
		c.JSON(http.StatusBadRequest, gin.H{"error": "account and password are required"})
		return
	}
	user, found, err := h.store.GetUser(req.Account)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load user"})
		return
	}
	if !found {
		c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid credentials"})
		return
	}
	if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)); err != nil {
		c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid credentials"})
		return
	}
	if user.Banned {
		writeBanJSON(c, user.BanReason)
		return
	}
	token, expiresAt, err := auth.GenerateToken(h.store.JWTSecret(), h.store.JWTIssuer(), user.Account, 7*24*time.Hour)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate token"})
		return
	}
	if cid, ok := models.NormalizeAuthClientID(req.ClientID); ok {
		name := models.ClampAuthClientName(req.ClientName)
		if rec, err := h.store.RecordAuthClient(req.Account, cid, name); err == nil {
			user = rec
		}
	}
	c.JSON(http.StatusOK, gin.H{
		"token":     token,
		"expiresAt": expiresAt.Format(time.RFC3339),
		"user":      user.OwnerPublic(),
	})
}

func (h *Handler) Verify(c *gin.Context) {
	var req verifyRequest
	if err := c.ShouldBindJSON(&req); err != nil || strings.TrimSpace(req.Token) == "" {
		c.JSON(http.StatusBadRequest, gin.H{"error": "token is required"})
		return
	}
	claims, err := auth.ParseToken(h.store.JWTSecret(), h.store.JWTIssuer(), req.Token)
	if err != nil {
		c.JSON(http.StatusUnauthorized, gin.H{"valid": false, "error": "invalid token"})
		return
	}
	user, found, err := h.store.GetUser(claims.Account)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load user"})
		return
	}
	if !found {
		c.JSON(http.StatusUnauthorized, gin.H{"valid": false, "error": "user not found"})
		return
	}
	if user.Banned {
		h := gin.H{"valid": false, "error": "account is banned"}
		if r := strings.TrimSpace(user.BanReason); r != "" {
			h["banReason"] = r
		}
		c.JSON(http.StatusOK, h)
		return
	}
	if cid, cname, ok := authClientFromHeaders(c); ok {
		_, _ = h.store.RecordAuthClient(claims.Account, cid, cname)
	}
	c.JSON(http.StatusOK, gin.H{"valid": true, "user": user.Public()})
}

func (h *Handler) Me(c *gin.Context) {
	token := bearerToken(c.GetHeader("Authorization"))
	if token == "" {
		c.JSON(http.StatusUnauthorized, gin.H{"error": "missing token"})
		return
	}
	claims, err := auth.ParseToken(h.store.JWTSecret(), h.store.JWTIssuer(), token)
	if err != nil {
		c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
		return
	}
	user, found, err := h.store.GetUser(claims.Account)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load user"})
		return
	}
	if !found {
		c.JSON(http.StatusUnauthorized, gin.H{"error": "user not found"})
		return
	}
	if abortIfUserBanned(c, user) {
		return
	}
	if cid, cname, ok := authClientFromHeaders(c); ok {
		if rec, err := h.store.RecordAuthClient(claims.Account, cid, cname); err == nil {
			user = rec
		}
	}
	today := models.CurrentActivityDate()
	nowAt := models.CurrentActivityTime()
	user, _, err = h.store.RecordVisit(claims.Account, today, nowAt)
	if err != nil {
		if errors.Is(err, os.ErrNotExist) {
			c.JSON(http.StatusUnauthorized, gin.H{"error": "user not found"})
			return
		}
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save visit"})
		return
	}
	visitIP := strings.TrimSpace(c.GetHeader("X-Visit-Ip"))
	visitLoc := strings.TrimSpace(c.GetHeader("X-Visit-Location"))
	if visitIP == "" {
		visitIP = strings.TrimSpace(c.ClientIP())
	}
	lookupURL := strings.TrimSpace(os.Getenv("GEO_LOOKUP_URL"))
	if lookupURL == "" {
		lookupURL = clientgeo.DefaultLookupURL
	}
	if visitLoc == "" && visitIP != "" {
		if loc, geoErr := clientgeo.FetchDisplayLocation(c.Request.Context(), lookupURL, visitIP); geoErr == nil {
			visitLoc = loc
		}
	}
	if visitIP != "" || visitLoc != "" {
		user, err = h.store.UpdateLastVisitMeta(claims.Account, visitIP, visitLoc)
		if err != nil {
			if errors.Is(err, os.ErrNotExist) {
				c.JSON(http.StatusUnauthorized, gin.H{"error": "user not found"})
				return
			}
			c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save visit meta"})
			return
		}
	}
	checkInConfig := h.store.CheckInConfig()
	c.JSON(http.StatusOK, gin.H{
		"user": user.OwnerPublic(),
		"checkIn": gin.H{
			"rewardCoins":     checkInConfig.RewardCoins,
			"checkedInToday":  user.LastCheckInDate == today,
			"lastCheckInDate": user.LastCheckInDate,
			"lastCheckInAt":   user.LastCheckInAt,
			"today":           today,
		},
	})
}
