package iam

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

	"github.com/gin-gonic/gin"
)

type Handler struct {
	service *Service
	cfg     Config
}

func NewHandler(service *Service, cfg Config) *Handler {
	return &Handler{service: service, cfg: cfg}
}

func (h *Handler) RegisterRoutes(router *gin.Engine) {
	auth := router.Group("/api/v1/auth")
	auth.POST("/register", h.register)
	auth.POST("/login", h.login)
	auth.POST("/refresh", h.refresh)

	protected := auth.Group("")
	protected.Use(h.service.RequireAccess())
	protected.POST("/logout", h.logout)
	protected.POST("/logout-all", h.logoutAll)
	protected.GET("/sessions", h.listSessions)
	protected.DELETE("/sessions/:sid", h.revokeSession)
}

func (h *Handler) register(c *gin.Context) {
	var input struct {
		Email     string `json:"email"`
		Password  string `json:"password"`
		AutoLogin *bool  `json:"auto_login"`
	}
	if err := c.ShouldBindJSON(&input); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	autoLogin := true
	if input.AutoLogin != nil {
		autoLogin = *input.AutoLogin
	}
	result, refreshToken, err := h.service.Register(c.Request.Context(), input.Email, input.Password, autoLogin, requestMetaFromContext(c))
	if err != nil {
		h.writeAuthError(c, err, "registration failed")
		return
	}
	if !autoLogin {
		c.JSON(http.StatusCreated, gin.H{"email": strings.TrimSpace(strings.ToLower(input.Email))})
		return
	}
	h.setRefreshCookie(c, refreshToken)
	c.JSON(http.StatusCreated, result)
}

func (h *Handler) login(c *gin.Context) {
	var input struct {
		Email      string `json:"email"`
		Password   string `json:"password"`
		DeviceInfo string `json:"device_info"`
	}
	if err := c.ShouldBindJSON(&input); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	meta := requestMetaFromContext(c)
	meta.DeviceInfo = input.DeviceInfo
	result, refreshToken, err := h.service.Login(c.Request.Context(), input.Email, input.Password, meta)
	if err != nil {
		h.writeAuthError(c, err, "login failed")
		return
	}
	h.setRefreshCookie(c, refreshToken)
	c.JSON(http.StatusOK, result)
}

func (h *Handler) refresh(c *gin.Context) {
	refreshToken, err := c.Cookie(h.cfg.RefreshCookieName)
	if err != nil || strings.TrimSpace(refreshToken) == "" {
		c.JSON(http.StatusUnauthorized, gin.H{"error": "missing refresh token"})
		return
	}
	result, newRefreshToken, err := h.service.Refresh(c.Request.Context(), refreshToken)
	if err != nil {
		h.writeAuthError(c, err, "refresh failed")
		return
	}
	h.setRefreshCookie(c, newRefreshToken)
	c.JSON(http.StatusOK, result)
}

func (h *Handler) logout(c *gin.Context) {
	token := extractBearerToken(c.GetHeader("Authorization"))
	if token == "" {
		c.JSON(http.StatusUnauthorized, gin.H{"error": "missing authorization"})
		return
	}
	if err := h.service.Logout(c.Request.Context(), token); err != nil {
		h.writeAuthError(c, err, "logout failed")
		return
	}
	h.clearRefreshCookie(c)
	c.Status(http.StatusNoContent)
}

func (h *Handler) logoutAll(c *gin.Context) {
	token := extractBearerToken(c.GetHeader("Authorization"))
	if token == "" {
		c.JSON(http.StatusUnauthorized, gin.H{"error": "missing authorization"})
		return
	}
	if err := h.service.LogoutAll(c.Request.Context(), token); err != nil {
		h.writeAuthError(c, err, "logout-all failed")
		return
	}
	h.clearRefreshCookie(c)
	c.Status(http.StatusNoContent)
}

func (h *Handler) listSessions(c *gin.Context) {
	uid := c.GetInt64(ContextUserIDKey)
	sessions, err := h.service.ListSessions(c.Request.Context(), uid)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list sessions"})
		return
	}
	c.JSON(http.StatusOK, sessions)
}

func (h *Handler) revokeSession(c *gin.Context) {
	uid := c.GetInt64(ContextUserIDKey)
	sid := strings.TrimSpace(c.Param("sid"))
	if sid == "" {
		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid sid"})
		return
	}
	if err := h.service.RevokeSession(c.Request.Context(), uid, sid); err != nil {
		h.writeAuthError(c, err, "revoke session failed")
		return
	}
	c.Status(http.StatusNoContent)
}

func requestMetaFromContext(c *gin.Context) requestMeta {
	return requestMeta{
		IP:        c.ClientIP(),
		UserAgent: c.Request.UserAgent(),
	}
}

func (h *Handler) writeAuthError(c *gin.Context, err error, fallback string) {
	switch {
	case errors.Is(err, errInvalidCredentials):
		c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid credentials"})
	case errors.Is(err, errAlreadyExists):
		c.JSON(http.StatusConflict, gin.H{"error": "user already exists"})
	case errors.Is(err, errInvalidToken), errors.Is(err, errTokenExpired), errors.Is(err, errUnauthorized), errors.Is(err, errSessionRevoked):
		c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
	case errors.Is(err, errForbidden):
		c.JSON(http.StatusForbidden, gin.H{"error": "forbidden"})
	default:
		c.JSON(http.StatusInternalServerError, gin.H{"error": fallback})
	}
}

func (h *Handler) setRefreshCookie(c *gin.Context, token string) {
	h.applySameSite(c)
	maxAge := int(h.cfg.RefreshTTL / time.Second)
	c.SetCookie(h.cfg.RefreshCookieName, token, maxAge, h.cfg.CookiePath, h.cfg.CookieDomain, h.cfg.CookieSecure, true)
}

func (h *Handler) clearRefreshCookie(c *gin.Context) {
	h.applySameSite(c)
	c.SetCookie(h.cfg.RefreshCookieName, "", -1, h.cfg.CookiePath, h.cfg.CookieDomain, h.cfg.CookieSecure, true)
}

func (h *Handler) applySameSite(c *gin.Context) {
	switch strings.ToLower(strings.TrimSpace(h.cfg.CookieSameSite)) {
	case "strict":
		c.SetSameSite(http.SameSiteStrictMode)
	case "none":
		c.SetSameSite(http.SameSiteNoneMode)
	default:
		c.SetSameSite(http.SameSiteLaxMode)
	}
}
