package api_key

import (
	"encoding/base64"
	"encoding/json"
	"fmt"
	"net"
	"regexp"
	"strings"
	"time"

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

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

var ErrInvalidApiKey = errors.New(InvalidAPIKey)

const (
	InvalidAPIKey  = "invalid API key"
	InvalidUser    = "invalid user"
	InvalidAPIPath = "invalid API path"
)

type ApiKeyJson struct {
	Key    string `json:"k"`
	UserId uint   `json:"u"`
	OrgId  uint   `json:"id"`
}

func Decode(keyString string) (*ApiKeyJson, error) {
	jsonString, err := base64.StdEncoding.DecodeString(keyString)
	if err != nil {
		return nil, ErrInvalidApiKey
	}

	var keyObj ApiKeyJson
	err = json.Unmarshal(jsonString, &keyObj)
	if err != nil {
		return nil, ErrInvalidApiKey
	}

	return &keyObj, nil
}

func IsValid(key *ApiKeyJson, hashedKey string) (bool, error) {
	check, err := utils.EncodePassword(key.Key, fmt.Sprintf("api_key%d", key.UserId))
	if err != nil {
		return false, err
	}
	return check == hashedKey, nil
}

var internalUserAllowUris = []string{
	"/clinic/api/v1/diag/precreate",
	"/clinic/api/v1/diag/upload",
	"/clinic/api/v1/diag/flush",
	"/clinic/api/v1/diagnosis/",
	"/clinic/api/v1/data/",
	"/clinic/api/v1/orgs",
	"/clinic/api/v1/reports",
	"/clinic/api/v1/activityhub",
	"/clinic/api/v1/dashboard/activeqps",
	"/clinic/api/v1/dashboard/clusters2",
	//"/clinic/api/v1/dashboard/clusters",
}

var internalUserAllowUrisRegex = []*regexp.Regexp{
	regexp.MustCompile(`/clinic/api/v1/orgs/[^\/]+/clusters`),
	regexp.MustCompile(`^/clinic/api/v1/orgs/[^\/]+/clusters/[^\/]+$`),
}

var externalUserAllowUris = []string{
	"/clinic/api/v1/diag/precreate",
	"/clinic/api/v1/diag/upload",
	"/clinic/api/v1/diag/flush",
}

var externalUserAllowUrisRegex = []*regexp.Regexp{}

// apiKeyAuthOnlyUris define uri can only use api key based auth.
// can be removed since we have internal api
var apiKeyAuthOnlyUris = []string{
	"/clinic/api/v1/diagnosis/",
	//"/clinic/api/v1/data/",
}

func isAllowUri(path string, allowUris []string, allowUrisRegexs []*regexp.Regexp) bool {
	for _, uri := range allowUris {
		if strings.HasPrefix(path, uri) {
			return true
		}
	}

	for _, r := range allowUrisRegexs {
		if r.MatchString(path) {
			return true
		}
	}

	return false
}

func getAllowUris(isInternal bool) ([]string, []*regexp.Regexp) {
	if isInternal {
		return internalUserAllowUris, internalUserAllowUrisRegex
	}

	return externalUserAllowUris, externalUserAllowUrisRegex
}

// MWTryAuth return ture indicates the whole auth check is completed,
// no need to try session based auth checking later.
func MWTryAuth(c *gin.Context, allowCidrs []*net.IPNet, disableIPWhitelist bool) bool {
	header := c.GetHeader("Authorization")
	parts := strings.SplitN(header, " ", 2)

	var keyString string
	if len(parts) == 2 && parts[0] == "Bearer" {
		keyString = parts[1]
	} else {
		username, password, err := utils.DecodeBasicAuthHeader(header)
		if err == nil && username == "api_key" {
			keyString = password
		}
	}

	if keyString == "" {
		// for those api only allow api key based auth, abort and return auth error directly.
		// return true to let caller skip session based auth.
		if isAllowUri(c.FullPath(), apiKeyAuthOnlyUris, nil) {
			log.Error("api key auth only uri", zap.String("path", c.FullPath()))
			utils.AuthError(c, InvalidAPIKey)
			return true
		}
		return false
	}

	decoded, err := Decode(keyString)
	if err != nil {
		log.Error("decode api key error", zap.Error(err))
		utils.AuthError(c, InvalidAPIKey)
		return true
	}

	// different way to verify credentials based on OrgId and path
	var userID uint
	var isInternalKey bool
	userID, err = VerifyAPIKey(decoded)
	if err != nil {
		if decoded.OrgId == 0 {
			// OrgId=0 indicates internal user credential
			userID, err = VerifyInternalKey(decoded)
			if err != nil {
				log.Error("verify internal key error", zap.Uint("user_id", decoded.UserId), zap.Error(err))
				utils.AuthError(c, err.Error())
				return true
			}
			isInternalKey = true
		} else if isAllowUri(c.FullPath(), externalUserAllowUris, externalUserAllowUrisRegex) {
			userID, err = VerifyUploadKey(decoded)
			if err != nil {
				log.Error("verify upload key error", zap.Uint("user_id", decoded.UserId), zap.Error(err))
				utils.AuthError(c, err.Error())
				return true
			}
		}
	}

	//credentials, _, err := model.ListCredentials(decoded.OrgId, decoded.UserId, 1, 10)
	//if err != nil || credentials == nil || len(credentials) == 0 {
	//	utils.AuthError(c, InvalidAPIKey)
	//	return true
	//}
	//
	//var cre *db.Credential
	//for _, credential := range credentials {
	//	isValid, err := IsValid(decoded, credential.HashedKey)
	//	if err == nil && isValid {
	//		cre = &credential
	//		break
	//	}
	//}
	//
	//if cre == nil {
	//	utils.AuthError(c, InvalidAPIKey)
	//	return true
	//}

	user, err := model.GetUserByID(userID)
	if err != nil || user == nil {
		utils.AuthError(c, InvalidUser)
		return true
	}

	// Internal user keys (OrgId=0) with verified internal user can access all APIs
	// Skip URI whitelist check for internal users using internal keys
	if isInternalKey {
		if !user.Internal {
			// The key claims to be internal but user is not internal
			utils.AuthError(c, InvalidAPIKey)
			return true
		}
		// Internal user with internal key can access all APIs, skip URI check
	} else {
		// if Authentication check pass, but api is not allowed for this user,
		// we should return auth error and skip session based auth.
		allowUris, allowUrisRegex := getAllowUris(user.Internal)
		if !isAllowUri(c.FullPath(), allowUris, allowUrisRegex) {
			utils.AuthError(c, InvalidAPIPath)
			return true
		}
	}

	u := &session.CtxUser{
		Username:        user.UserName,
		UserID:          user.ID,
		Email:           user.Email,
		Internal:        user.Internal,
		OrgIDInApiToken: decoded.OrgId,
	}

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

	c.Set(session.CtxUserKey, u)
	c.Next()

	return true
}

func VerifyUploadKey(decoded *ApiKeyJson) (uint, error) {
	credentials, _, err := model.ListCredentials(decoded.OrgId, decoded.UserId, 1, 10)
	if err != nil || credentials == nil || len(credentials) == 0 {
		return 0, ErrInvalidApiKey
	}

	var cre *db.Credential
	for _, credential := range credentials {
		isValid, err := IsValid(decoded, credential.HashedKey)
		if err == nil && isValid {
			cre = &credential
			break
		}
	}
	if cre == nil {
		return 0, ErrInvalidApiKey
	}
	return cre.UserID, nil
}

func VerifyAPIKey(decoded *ApiKeyJson) (uint, error) {
	credentials, _, err := model.ListAPICredentials(decoded.UserId, 0, 0)
	if err != nil || credentials == nil || len(credentials) == 0 {
		return 0, ErrInvalidApiKey
	}

	var cre *db.APICredential
	for _, credential := range credentials {
		if decoded.Key == credential.PlainKey {
			cre = &credential
			break
		}
	}

	if cre == nil {
		return 0, ErrInvalidApiKey
	}
	return cre.UserID, nil
}

// VerifyInternalKey verifies an internal user API key (OrgId=0).
func VerifyInternalKey(decoded *ApiKeyJson) (uint, error) {
	credentials, err := model.ListActiveInternalAPICredentials(decoded.UserId)
	if err != nil || len(credentials) == 0 {
		return 0, ErrInvalidApiKey
	}

	for _, cred := range credentials {
		// Check if credential has expired
		if cred.ExpiresAt != nil && time.Now().After(*cred.ExpiresAt) {
			continue
		}

		isValid, err := IsValid(decoded, cred.HashedKey)
		if err != nil {
			continue
		}
		if isValid {
			// Update last used time asynchronously
			go func(credID uint) {
				if err := model.UpdateInternalCredentialLastUsed(credID); err != nil {
					log.Error("failed to update internal credential last used time",
						zap.Uint("credential_id", credID), zap.Error(err))
				}
			}(cred.ID)
			return cred.UserID, nil
		}
	}
	return 0, ErrInvalidApiKey
}
