package session

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

	"github.com/form3tech-oss/jwt-go"
	"github.com/gin-gonic/gin"
	"github.com/pingcap/errors"
	"github.com/pingcap/log"
	"go.uber.org/zap"

	"github.com/pingcap-inc/clinic/pkg/auth/utils"
	cfg2 "github.com/pingcap-inc/clinic/pkg/config"
	"github.com/pingcap-inc/clinic/pkg/db/model"
)

const (
	sessionTimeout    = 7 * 24 * time.Hour
	sessionCookieName = "clinic.session_id"

	InvalidIP = "IP is not allowed"
)

var UsersSession = make(map[string]*UserSession)

type sessionTokenBody struct {
	jwt.StandardClaims
	Session *Session `json:"user"`
}

type SessionManager struct {
	sessionSecret      []byte
	disabled           bool
	secure             bool
	allowCidrs         []*net.IPNet
	disableIPWhitelist bool
}

func NewSessionManager(cfg cfg2.SessionConfig, allowCidrs []*net.IPNet, disableIPWhitelist bool) *SessionManager {
	return &SessionManager{
		sessionSecret:      []byte(cfg.Secret),
		disabled:           cfg.Disabled,
		secure:             cfg.Secure,
		allowCidrs:         allowCidrs,
		disableIPWhitelist: disableIPWhitelist,
	}
}

func (sm *SessionManager) NewSession(session *Session) (string, error) {
	tokenBody := sessionTokenBody{
		Session: session,
		StandardClaims: jwt.StandardClaims{
			ExpiresAt: time.Now().Add(sessionTimeout).Unix(),
		},
	}
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, tokenBody)
	tokenString, err := token.SignedString(sm.sessionSecret)
	if err != nil {
		return "", err
	}
	return tokenString, nil
}

func (sm *SessionManager) getSession(sessionID string) (*Session, error) {
	if sessionID == "" {
		return nil, errors.New("sign in is required")
	}
	token, err := jwt.ParseWithClaims(
		sessionID,
		&sessionTokenBody{},
		func(token *jwt.Token) (any, error) {
			return sm.sessionSecret, nil
		})
	if err != nil {
		return nil, err
	}
	tokenBody, ok := token.Claims.(*sessionTokenBody)
	if !ok || !token.Valid {
		return nil, errors.Annotate(err, "session is expired")
	}
	return tokenBody.Session, nil
}

var IgnoreUris = []string{
	"/clinic/api/v1/login",
}

var skipCsrfToken = []string{
	"/grafana",
	"/kibana",
	"/metric/query",
	"/metric/series",
	"/metric/o11y/query",
	"/metric/node/query",
	"/metric/project/query",
	"/metric/base/query",
	"/metric/o11y_infra/series",
	"/log/query",
}

// verify auth via session cookie
func (sm *SessionManager) MWTryAuth(c *gin.Context) {
	for _, uri := range IgnoreUris {
		if strings.HasPrefix(c.Request.RequestURI, uri) {
			c.Next()
			return
		}
	}

	sessionID, err := c.Cookie(sessionCookieName)
	if err != nil || sessionID == "" {
		redirect(c)
		return
	}

	sess, err := sm.getSession(sessionID)
	if err != nil {
		log.Error("get session failed", zap.Error(err))
		redirect(c)
		return
	}

	if sess == nil || sess.Username == "" {
		log.Error("empty session, need relogin")
		redirect(c)
		return
	}

	if !IsValidCsrfToken(c, sess) {
		log.Error("missing or incorrect x-csrf-token", zap.String("user", sess.Username))
		redirect(c)
		return
	}

	u, err := model.GetUserByUserName(sess.Username)
	if err != nil {
		log.Error("get user failed", zap.Error(err))
		redirect(c)
		return
	}

	ctxUser := &CtxUser{
		Username: u.UserName,
		UserID:   u.ID,
		Email:    u.Email,
		Internal: u.Internal,
		// OrgID:    oid,
	}

	if NeedToLimitIP(ctxUser, c.FullPath()) {
		if !AllowIP(sm.allowCidrs, SourceIPs(c.GetHeader("X-Forwarded-For")), sm.disableIPWhitelist) {
			utils.ForbiddenError(c, InvalidIP)
			return
		}
	}

	c.Set(CtxUserKey, ctxUser)
	c.Next()
}

func redirect(c *gin.Context) {
	if strings.HasPrefix(c.Request.RequestURI, "/grafana") || strings.HasPrefix(c.Request.RequestURI, "/kibana") {
		c.Header("Cache-Control", "no-cache")
		c.Redirect(http.StatusFound, "/portal/#/login?next="+url.QueryEscape("https://"+c.Request.Host+c.Request.RequestURI))
		c.Abort()
		return
	}

	utils.AuthError(c, "Unauthorized")
}

func IsValidCsrfToken(c *gin.Context, sess *Session) bool {
	for _, u := range skipCsrfToken {
		if strings.HasPrefix(c.Request.RequestURI, u) {
			return true
		}
	}

	if strings.HasPrefix(c.Request.RequestURI, "/clinic/api/v1/orgs") &&
		(strings.Contains(c.Request.RequestURI, "/downloadConf/") || strings.Contains(c.Request.RequestURI, "/download")) {
		return true
	}

	if strings.HasPrefix(c.Request.RequestURI, "/clinic/api/v1/dashboard/proxy") && strings.Contains(c.Request.RequestURI, "/download") {
		return true
	}

	if strings.Contains(c.Request.RequestURI, "/profiling/single/view") {
		return true
	}

	if strings.Contains(c.Request.RequestURI, "/continuous_profiling/single_profile") {
		return true
	}

	if strings.Contains(c.Request.RequestURI, "/dashboard/reports") {
		return true
	}

	csrfToken := c.Request.Header.Get("x-csrf-token")
	return csrfToken == sess.CSRFToken
}

func (sm *SessionManager) GetSessionUser(ctx *gin.Context) (*Session, error) {
	sessionID, err := ctx.Cookie(sessionCookieName)
	if err != nil {
		return nil, err
	}

	return sm.getSession(sessionID)
}

func (sm *SessionManager) SetCookie(ctx *gin.Context, sessionKey string) {
	sm.setCookie(ctx, sessionCookieName, sessionKey)
}

func (sm *SessionManager) setCookie(ctx *gin.Context, name, sessionKey string) {
	ctx.SetCookie(name, sessionKey, int(sessionTimeout/time.Second), "/", "", sm.secure, false)
}

func (sm *SessionManager) ClearCookie(ctx *gin.Context) {
	ctx.SetCookie(sessionCookieName, "", -1, "/", "", sm.secure, false)
}

func GetUser(c *gin.Context) *CtxUser {
	ui, ok := c.Get(CtxUserKey)
	if !ok {
		return nil
	}
	u, ok := ui.(*CtxUser)
	if !ok {
		return nil
	}
	return u
}

func GetDbaasUser(c *gin.Context) *CtxDbaasUser {
	ui, ok := c.Get(DbaasUserKey)
	if !ok {
		return nil
	}
	u, ok := ui.(*CtxDbaasUser)
	if !ok {
		return nil
	}
	return u
}
