package auth

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

	"github.com/gin-gonic/gin"

	"gitlab.com/dynaweb/ains-microservices/pkg/config"
	"gitlab.com/dynaweb/ains-microservices/pkg/jwt"
	"gitlab.com/dynaweb/ains-microservices/pkg/oauth2"
)

type Handler struct {
	svc    *Service
	google *oauth2.Google
	cfg    *config.Config
}

func NewHandler(svc *Service, google *oauth2.Google, cfg *config.Config) *Handler {
	return &Handler{svc: svc, google: google, cfg: cfg}
}

func (h *Handler) Register(r gin.IRouter) {
	r.GET("/auth/login", h.login)
	r.GET("/auth/google/callback", h.callback)
	r.GET("/auth/me", h.me)
	r.POST("/auth/logout", h.logout)
	r.POST("/auth/refresh", h.refresh)
}

func (h *Handler) login(c *gin.Context) {
	state := newToken()
	setStateCookie(c, h.cfg, state)

	if h.cfg.Google.Mock || !h.google.Configured() {
		q := url.Values{
			"state":   {state},
			"email":   {defaultEmail(c.Query("email"))},
			"name":    {c.Query("name")},
			"picture": {c.Query("picture")},
		}
		c.Redirect(http.StatusFound, "/auth/google/callback?"+q.Encode())
		return
	}

	authURL, err := h.google.AuthCodeURL(state)
	if err != nil {
		c.String(http.StatusInternalServerError, "oauth not configured")
		return
	}
	c.Redirect(http.StatusFound, authURL)
}

func (h *Handler) callback(c *gin.Context) {
	if !h.validState(c) {
		c.String(http.StatusBadRequest, "invalid oauth state")
		return
	}

	info, err := h.exchange(c)
	if err != nil {
		c.String(http.StatusBadGateway, "authentication failed")
		return
	}

	sess, err := h.svc.CompleteLogin(c.Request.Context(), info, "")
	if err != nil {
		if errors.Is(err, ErrRoleNotAllowed) {
			ClearAuthCookies(c, h.cfg)
			c.Redirect(http.StatusFound, "/web/auth-failed")
			return
		}
		c.String(http.StatusInternalServerError, "login failed")
		return
	}

	SetAuthCookies(c, h.cfg, sess.AccessToken, sess.RefreshToken)
	clearCookie(c, stateCookieName, h.cfg.Auth.CookieDomain)
	c.Redirect(http.StatusFound, sess.Redirect)
}

func (h *Handler) exchange(c *gin.Context) (*oauth2.UserInfo, error) {
	if h.cfg.Google.Mock || !h.google.Configured() {
		return &oauth2.UserInfo{
			Email:   defaultEmail(c.Query("email")),
			Name:    c.Query("name"),
			Picture: c.Query("picture"),
		}, nil
	}
	code := c.Query("code")
	if code == "" {
		return nil, errors.New("missing code")
	}
	return h.google.Exchange(c.Request.Context(), code)
}

func (h *Handler) me(c *gin.Context) {
	claims, err := h.ParseAccess(c)
	if err != nil {
		c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
		return
	}
	c.JSON(http.StatusOK, gin.H{
		"user_id": claims.UserID,
		"role":    claims.Role,
		"state":   claims.State,
		"ppd":     claims.PPD,
		"school":  claims.School,
		"expires": claims.ExpiresAt,
	})
}

func (h *Handler) logout(c *gin.Context) {
	if claims, err := h.ParseAccess(c); err == nil && claims.ID != "" {
		if exp := claims.ExpiresAt; exp != nil {
			ttl := time.Until(exp.Time)
			if ttl > 0 {
				_ = h.svc.denylist.Revoke(c.Request.Context(), claims.ID, ttl)
			}
		}
	}
	ClearAuthCookies(c, h.cfg)
	c.JSON(http.StatusOK, gin.H{"status": "ok"})
}

func (h *Handler) refresh(c *gin.Context) {
	raw := h.tokenFromRequest(c, refreshCookieName(h.cfg))
	if raw == "" {
		c.JSON(http.StatusUnauthorized, gin.H{"error": "missing refresh token"})
		return
	}
	claims, err := h.svc.tokens.ParseRefresh(raw)
	if err != nil {
		c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid refresh token"})
		return
	}
	if claims.ID != "" {
		if revoked, _ := h.svc.denylist.Revoked(c.Request.Context(), claims.ID); revoked {
			c.JSON(http.StatusUnauthorized, gin.H{"error": "revoked"})
			return
		}
	}
	access, err := h.svc.tokens.IssueAccess(jwt.Identity{
		UserID: claims.UserID,
		Role:   claims.Role,
		State:  claims.State,
		PPD:    claims.PPD,
		School: claims.School,
	})
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "cannot issue token"})
		return
	}
	setCookie(c, accessCookieName(h.cfg), access, int(h.cfg.JWT.AccessTTL.Seconds()), h.cfg.Auth.CookieDomain, h.cfg.Auth.CookieSecure)
	c.JSON(http.StatusOK, gin.H{"status": "ok"})
}

func (h *Handler) ParseAccess(c *gin.Context) (*jwt.Claims, error) {
	raw := h.tokenFromRequest(c, accessCookieName(h.cfg))
	if raw == "" {
		return nil, errors.New("missing access token")
	}
	claims, err := h.svc.tokens.ParseAccess(raw)
	if err != nil {
		return nil, err
	}
	if claims.ID != "" {
		if revoked, _ := h.svc.denylist.Revoked(c.Request.Context(), claims.ID); revoked {
			return nil, errors.New("revoked")
		}
	}
	return claims, nil
}

func (h *Handler) tokenFromRequest(c *gin.Context, cookieName string) string {
	if cookie, err := c.Cookie(cookieName); err == nil && cookie != "" {
		return cookie
	}
	header := c.GetHeader("Authorization")
	if strings.HasPrefix(header, "Bearer ") {
		return strings.TrimSpace(strings.TrimPrefix(header, "Bearer "))
	}
	return ""
}

func (h *Handler) validState(c *gin.Context) bool {
	cookie, err := c.Cookie(stateCookieName)
	if err != nil || cookie == "" {
		return false
	}
	return cookie == c.Query("state")
}

func defaultEmail(email string) string {
	if email != "" {
		return email
	}
	return "m-test@moe-dl.edu.my"
}
