package session

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

	"github.com/Kong/go-pdk"
	"github.com/form3tech-oss/jwt-go"
	"github.com/pingcap/errors"

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

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

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",
}

var (
	getUserByUserNameFunc = model.GetUserByUserName
	needToLimitIPFunc     = session.NeedToLimitIP
)

const (
	sessionCookieName    = "clinic.session_id"
	AUTH_ERROR_CODE      = 401
	FORBIDDEN_ERROR_CODE = 403

	InvalidAPIPath      = "Invalid User"
	PermissionForbidden = "Permission Deny"
)

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

func NewSessionManager(cfg *config.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) 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
}

func (sm *SessionManager) MWTryAuth(kong *pdk.PDK) (*session.CtxUser, *utils2.AuthErr) {
	// responseHeaders := make(map[string][]string)
	// responseHeaders["Content-Type"] = append(responseHeaders["Content-Type"], "application/json")

	uri, err := utils2.GetURI(kong)
	if err != nil {
		_ = kong.Log.Err("get path failed", err.Error())
		// kong.Response.Exit(AUTH_ERROR_CODE, InvalidAPIPath, responseHeaders)

		return nil, utils2.NewAuthErr(AUTH_ERROR_CODE, InvalidAPIPath)
	}

	for _, ignoreUri := range IgnoreUris {
		if strings.HasPrefix(uri, ignoreUri) {
			// c.Next()
			_ = kong.ServiceRequest.SetHeader(session.AuthType, "api-gateway")
			return nil, nil
		}
	}

	sessionID, err := Cookie(kong, sessionCookieName)
	if err != nil || sessionID == "" {
		_ = kong.Log.Err("get session failed", err.Error())
		return nil, redirect(uri, kong)
	}

	sess, err := sm.getSession(sessionID)
	if err != nil {
		_ = kong.Log.Err("get session failed", err.Error())
		return nil, redirect(uri, kong)
	}

	if sess == nil || sess.Username == "" {
		_ = kong.Log.Err("empty session, need relogin")
		return nil, redirect(uri, kong)
	}

	if !IsValidCsrfToken(uri, kong, sess) {
		_ = kong.Log.Err("missing or incorrect x-csrf-token", fmt.Sprintf("user=%v", sess.Username))
		return nil, redirect(uri, kong)
	}

	u, err := getUserByUserNameFunc(sess.Username)
	if err != nil {
		_ = kong.Log.Err("fetch user failed", fmt.Sprintf("user=%v", sess.Username), err.Error())
		return nil, redirect(uri, kong)
	}

	sessUser := &session.CtxUser{
		Username:   u.UserName,
		UserID:     u.ID,
		Email:      u.Email,
		Internal:   u.Internal,
		AuthMethod: session.AuthMethodUISession,
	}

	if needToLimitIPFunc(sessUser, uri) {
		ip, err := kong.Client.GetForwardedIp()
		if err != nil {
			_ = kong.Log.Err("get ip failed", err.Error())
			return sessUser, utils2.NewAuthErr(FORBIDDEN_ERROR_CODE, session.InvalidIP)
		}
		if !session.AllowIP(sm.allowCidrs, session.SourceIPs(ip), sm.disableIPWhitelist) {
			return sessUser, utils2.NewAuthErr(FORBIDDEN_ERROR_CODE, session.InvalidIP)
		}
	}

	return sessUser, nil
}

func redirect(uri string, kong *pdk.PDK) *utils2.AuthErr {
	responseHeaders := make(map[string][]string)
	responseHeaders["Content-Type"] = append(responseHeaders["Content-Type"], "application/json")

	if strings.HasPrefix(uri, "/grafana") || strings.HasPrefix(uri, "/kibana") {
		// c.Header("Cache-Control", "no-cache")
		// c.Redirect(http.StatusFound, "/portal")
		// c.Abort()

		host, _ := kong.Request.GetHost()
		path, _ := kong.Request.GetPathWithQuery()
		next := url.QueryEscape("https://" + host + path)

		responseHeaders["Location"] = []string{"/portal/#/login?next=" + next}
		_ = kong.Response.SetHeader("Cache-Control", "no-cache")

		kong.Response.Exit(http.StatusFound, []byte{}, responseHeaders)
		// return utils2.NewAuthErr(http.StatusFound, InvalidAPIPath)
	}

	return utils2.NewAuthErr(AUTH_ERROR_CODE, InvalidAPIPath)
	// kong.Response.Exit(AUTH_ERROR_CODE, InvalidAPIPath, responseHeaders)
}

func IsValidCsrfToken(uri string, kong *pdk.PDK, sess *Session) bool {
	for _, u := range skipCsrfToken {
		if strings.HasPrefix(uri, u) {
			return true
		}
	}

	// tmp workaround: need to remove soon
	if strings.HasPrefix(uri, "/ngm") {
		return true
	}

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

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

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

	if strings.Contains(uri, "/continuous_profiling/single_profile") {
		return true
	}

	if strings.Contains(uri, "/dashboard/reports") {
		return true
	}

	csrfToken, err := kong.Request.GetHeader("x-csrf-token")
	if err != nil {
		_ = kong.Log.Err("get csrf token failed", err.Error())
		return false
	}

	_ = kong.Log.Info(fmt.Sprintf("csrf-token, session=%s, header=%s", sess.CSRFToken, csrfToken))
	return csrfToken == sess.CSRFToken
}
