package api_key

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

	"github.com/Kong/go-pdk"
	"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/auth/utils"
	"github.com/pingcap-inc/clinic/pkg/db"
	"github.com/pingcap-inc/clinic/pkg/db/model"
)

var ErrInvalidApiKey = errors.New("invalid API key")

const (
	InvalidAPIKey = "invalid API key"
	InvalidUser   = "invalid user"

	InvalidAPIPath = "invalid API path"
)

const (
	AUTH_ERROR_CODE      = 401
	FORBIDDEN_ERROR_CODE = 403
)

var (
	verifyAPIKeyFunc      = VerifyAPIKey
	verifyInternalKeyFunc = VerifyInternalKey
	verifyUploadKeyFunc   = VerifyUploadKey
	getUserByIDFunc       = model.GetUserByID
)

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

type VerifiedCredential struct {
	UserID uint
	Type   string
	ID     uint
	Name   string
}

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/diagnosis_activity/",
	"/clinic/api/v1/data/",
	"/clinic/api/v1/orgs",
	"/clinic/api/v1/reports",
	"/clinic/api/v1/dashboard/activeqps",
	//"/clinic/api/v1/dashboard/clusters",
	"/clinic/api/v1/dashboard/clusters2",
	"/clinic/api/v1/activityhub",
	"/ngm/",
	"/fault/",
	"/chaos/",
}

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.
var ApiKeyAuthOnlyUris = []string{
	"/clinic/api/v1/diagnosis/",
	// "/clinic/api/v1/data/",
}

var SkipAuthUris = []string{
	"/api/byoc/v1/",
}

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

	for _, r := range allowUrisRegex {
		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 later.
func MWTryAuth(kong *pdk.PDK, allowCidrs []*net.IPNet, disableIPWhitelist bool) (bool, *session.CtxUser, *utils2.AuthErr) {
	// responseHeaders := make(map[string][]string)
	// responseHeaders["Content-Type"] = append(responseHeaders["Content-Type"], "application/json")

	var keyString string
	if token, exist := utils2.GetBearToken(kong); exist {
		keyString = token
	} else {
		// parse the Authorization Header as basic auth header format.
		header, err := kong.Request.GetHeader("Authorization")
		if err != nil {
			return false, nil, nil
		}
		username, password, err := utils.DecodeBasicAuthHeader(header)
		if err == nil && username == "api_key" {
			keyString = password
		}
	}
	path, err := utils2.GetURI(kong)
	if err != nil {
		_ = kong.Log.Err("get path failed: ", err.Error())
		return true, nil, utils2.NewAuthErr(AUTH_ERROR_CODE, InvalidUser)
	}

	if keyString == "" {
		if IsAllowUri(path, SkipAuthUris, nil) {
			return true, &session.CtxUser{}, nil
		}
		// 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(path, ApiKeyAuthOnlyUris, nil) {
			_ = kong.Log.Err("api key is required for this api: ", path)
			return true, nil, utils2.NewAuthErr(AUTH_ERROR_CODE, InvalidAPIKey)
		}
		return false, nil, nil
	}

	decoded, err := Decode(keyString)
	if err != nil {
		_ = kong.Log.Err("Decode key failed: ", err.Error())
		// kong.Response.SetStatus(AUTH_ERROR_CODE)
		// kong.Response.Exit(AUTH_ERROR_CODE, InvalidAPIKey, responseHeaders)
		return false, nil, nil
	}

	if decoded.UserId == 0 {
		return false, nil, nil
	}

	// different way to verify credentials based on OrgId and path
	var credential *VerifiedCredential
	var isInternalKey bool
	credential, err = verifyAPIKeyFunc(decoded)
	if err != nil {
		if decoded.OrgId == 0 {
			// OrgId=0 indicates internal user credential
			credential, err = verifyInternalKeyFunc(decoded)
			if err != nil {
				_ = kong.Log.Err("verify internal key failed: ", err.Error())
				return true, nil, utils2.NewAuthErr(AUTH_ERROR_CODE, err.Error())
			}
			isInternalKey = true
		} else if IsAllowUri(path, externalUserAllowUris, externalUserAllowUrisRegex) {
			credential, err = verifyUploadKeyFunc(decoded)
			if err != nil {
				_ = kong.Log.Err("verify upload key failed: ", err.Error())
				return true, nil, utils2.NewAuthErr(AUTH_ERROR_CODE, err.Error())
			}
		} else {
			_ = kong.Log.Err("verify api key failed: ", err.Error())
			return true, nil, utils2.NewAuthErr(AUTH_ERROR_CODE, InvalidAPIKey)
		}
	}
	if credential == nil || credential.UserID == 0 {
		return true, nil, utils2.NewAuthErr(AUTH_ERROR_CODE, InvalidAPIKey)
	}

	sess := &session.CtxUser{
		UserID:          credential.UserID,
		OrgIDInApiToken: decoded.OrgId,
		AuthMethod:      session.AuthMethodAPIKey,
		CredentialType:  credential.Type,
		CredentialID:    credential.ID,
		CredentialName:  credential.Name,
	}

	user, err := getUserByIDFunc(credential.UserID)
	if err != nil || user == nil {
		return true, sess, utils2.NewAuthErrWithErr(AUTH_ERROR_CODE, InvalidUser, err)
	}

	sess.Username = user.UserName
	sess.UserID = user.ID
	sess.Email = user.Email
	sess.Internal = user.Internal

	// 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
			return true, sess, utils2.NewAuthErr(AUTH_ERROR_CODE, InvalidAPIKey)
		}
		// 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(path, allowUris, allowUrisRegex) {
			_ = kong.Log.Warn("path is not allow, ", path)
			return true, sess, utils2.NewAuthErr(AUTH_ERROR_CODE, InvalidAPIPath)
		}
	}

	ip, err := kong.Client.GetForwardedIp()
	if err != nil {
		_ = kong.Log.Err("get ip failed, ", err.Error())
		// kong.Response.Exit(FORBIDDEN_ERROR_CODE, session.InvalidIP, responseHeaders)
		return true, sess, utils2.NewAuthErr(FORBIDDEN_ERROR_CODE, session.InvalidIP)
	}

	if session.NeedToLimitIP(sess, path) {
		if !session.AllowIP(allowCidrs, session.SourceIPs(ip), disableIPWhitelist) {
			// kong.Response.Exit(FORBIDDEN_ERROR_CODE, session.InvalidIP, responseHeaders)
			return true, sess, utils2.NewAuthErr(FORBIDDEN_ERROR_CODE, session.InvalidIP)
		}
	}

	return true, sess, nil
}

func VerifyUploadKey(decoded *ApiKeyJson) (*VerifiedCredential, error) {
	credentials, _, err := model.ListCredentials(decoded.OrgId, decoded.UserId, 1, 10)
	if err != nil || credentials == nil || len(credentials) == 0 {
		return nil, 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 nil, ErrInvalidApiKey
	}
	return &VerifiedCredential{
		UserID: cre.UserID,
		Type:   session.CredentialTypeUpload,
		ID:     cre.ID,
	}, nil
}

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

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

	if cre == nil {
		return nil, ErrInvalidApiKey
	}
	return &VerifiedCredential{
		UserID: cre.UserID,
		Type:   session.CredentialTypeAPI,
		ID:     cre.ID,
	}, nil
}

// VerifyInternalKey verifies an internal user API key (OrgId=0).
func VerifyInternalKey(decoded *ApiKeyJson) (*VerifiedCredential, error) {
	credentials, err := model.ListActiveInternalAPICredentials(decoded.UserId)
	if err != nil || len(credentials) == 0 {
		return nil, 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) {
				_ = model.UpdateInternalCredentialLastUsed(credID)
			}(cred.ID)
			return &VerifiedCredential{
				UserID: cred.UserID,
				Type:   session.CredentialTypeInternal,
				ID:     cred.ID,
				Name:   cred.Name,
			}, nil
		}
	}
	return nil, ErrInvalidApiKey
}
