package whitelist

import (
	"context"
	"database/sql"
	"fmt"
	"github.com/pingcap/log"
	"github.com/pingcap/tep/db/entity"
	"github.com/pingcap/tep/pkg/apis/apiserver"
	"github.com/pingcap/tep/pkg/i18n"
	"github.com/pingcap/tep/pkg/rbac"
	store "github.com/pingcap/tep/pkg/registry/apiserver"
	"github.com/pingcap/tep/pkg/utils"
	"go.uber.org/zap"
	"k8s.io/apimachinery/pkg/api/errors"
	metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/apiserver/pkg/endpoints/request"
	"k8s.io/apiserver/pkg/registry/rest"
	"net"
	"regexp"
	"strconv"
	"strings"
	"sync"
	"time"
)

const tepWhitelistAlias = "_tep_platform_ip"
const whitelistNamePrefix = "whitelist-"

var (
	WhitelistNameRegexp = regexp.MustCompile(`^[A-Za-z0-9]{1,16}$`)
)

type Storage struct {
	lock     sync.Mutex
	store    *store.Store
	resource schema.GroupResource
}

func NewStorage(store *store.Store, resource schema.GroupResource) *Storage {
	return &Storage{
		store:    store,
		resource: resource,
	}
}

func (s *Storage) Kind() string {
	return "Whitelist"
}

func (s *Storage) New() runtime.Object {
	return &apiserver.Whitelist{}
}

func (s *Storage) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
	tidbClusterID := request.NamespaceValue(ctx)
	tc, err := s.store.GetTiDBCluster(tidbClusterID)
	if err != nil {
		return nil, err
	}

	db, err := s.store.GetTiDBConnection(tc)
	if err == nil {
		id, err := getWhitelistID(name)
		if err != nil {
			return nil, errors.NewBadRequest(fmt.Sprintf("invalid name format: %s", name))
		}
		var ipList, alias string
		dbErr := db.QueryRow("select name, list from whitelist where id=?", id).Scan(&alias, &ipList)
		if dbErr == sql.ErrNoRows {
			return nil, errors.NewNotFound(s.resource, fmt.Sprintf("whilelist <%s> is not found", name))
		}
		return &apiserver.Whitelist{ObjectMeta: metav1.ObjectMeta{Name: name, UID: types.UID(name)}, Alias: alias, Ips: strings.Split(ipList, ",")}, nil
	}
	return nil, err
}

func (s *Storage) List(ctx context.Context, options *metainternalversion.ListOptions, extraOptions runtime.Object) (runtime.Object, error) {
	tidbClusterID := request.NamespaceValue(ctx)
	tc, err := s.store.GetTiDBCluster(tidbClusterID)
	if err != nil {
		if errors.IsBadRequest(err) {
			return s.NewList(), nil
		}
		return nil, err
	}

	db, err := s.store.GetTiDBConnection(tc)
	lists := &apiserver.WhitelistList{Items: []apiserver.Whitelist{}}
	if err == nil {
		if err := utils.DoInTransaction(func(tx *sql.Tx) error {
			opts := extraOptions.(*apiserver.ListOptions)
			size := utils.GetSize(opts.ItemsPerPage)
			startIndex := utils.GetStartIndex(opts.Page, size)
			rows, err := tx.Query(fmt.Sprintf("select id, name, list from whitelist limit %d, %d", startIndex, size))
			if err != nil {
				log.Error("query white list failed", zap.Error(err))
				return errors.NewInternalError(err)
			}
			defer utils.CallDeferMethod(rows.Close, "close rows failed")
			var alias, list string
			var id int64
			for rows.Next() {
				if err = rows.Scan(&id, &alias, &list); err == nil {
					name := formatWhitelistName(id)
					lists.Items = append(lists.Items, apiserver.Whitelist{ObjectMeta: metav1.ObjectMeta{Name: name, UID: types.UID(name)}, Alias: alias, Ips: strings.Split(list, ",")})
				}
			}
			var total int64
			if err := tx.QueryRow("SELECT count(*) from whitelist").Scan(&total); err != nil {
				log.Error("query audit setting total failed", zap.Error(err))
				return errors.NewInternalError(err)
			}
			lists.Total = total
			return nil
		}, db); err != nil {
			return nil, err
		}
		return lists, nil
	}
	return lists, err
}

func (s *Storage) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) {
	startTime := time.Now()
	operatorName := utils.GetAPIUsername(ctx)
	if !rbac.CanOperate(s.store.DB, operatorName, rbac.OtherClusterOperation) {
		return nil, errors.NewForbidden(s.resource, operatorName, fmt.Errorf("permission denied"))
	}
	s.lock.Lock()
	defer s.lock.Unlock()

	tidbClusterID := request.NamespaceValue(ctx)
	tc, err := s.store.GetTiDBCluster(tidbClusterID)
	if err != nil {
		return nil, err
	}

	whitelist := obj.(*apiserver.Whitelist)
	if whitelist.Name == tepWhitelistAlias {
		return nil, errors.NewBadRequest(fmt.Sprintf("invalid whitelist name, %s is reserved", tepWhitelistAlias))
	}
	if err := s.Validate(whitelist); err != nil {
		return nil, err
	}

	db, err := s.store.GetTiDBConnection(tc)
	if err == nil {
		err := utils.DoInTransaction(func(tx *sql.Tx) error {
			var ipList string
			dbErr := db.QueryRow("select list from whitelist where name=?", whitelist.Alias).Scan(&ipList)
			if dbErr == nil && ipList != "" {
				return errors.NewBadRequest("already have a whitelist named " + whitelist.Alias)
			}

			_, err := tx.Exec("insert into whitelist(name, list) values(?,?)", whitelist.Alias, strings.Join(whitelist.Ips, ","))
			if err != nil {
				log.Error("add white list failed", zap.Error(err))
				return errors.NewInternalError(err)
			}

			dbErr = db.QueryRow("select list from whitelist where name=?", tepWhitelistAlias).Scan(&ipList)
			if dbErr == sql.ErrNoRows {
				log.Info(fmt.Sprintf("whilelist %s is not found, add default tep ip", tepWhitelistAlias))
				_, err := tx.Exec("insert into whitelist(name, list) values(?,?)", tepWhitelistAlias, s.store.ServerCIDR)
				if err != nil {
					log.Error("add tep whitelist failed", zap.Error(err))
					return errors.NewInternalError(err)
				}
			}
			return nil
		}, db)
		if err != nil {
			return nil, err
		}
		err = s.store.AddEvent(&utils.Event{
			UserID:         operatorName,
			Payload:        whitelist,
			OperationType:  entity.OperateOperation,
			ManagementType: entity.InstanceManagement,
			CostTime:       time.Now().Sub(startTime).Nanoseconds(),
		})
		if err != nil {
			return nil, err
		}
		err = s.store.FlushPlugin(db, store.WhitelistPlugin)
		if err != nil {
			return nil, err
		}
		i18n.PlatformMessage.AddPlatformMessageAndLogError(s.store.DB, i18n.OperateMessageType, i18n.UpdateClusterWhitelist, "add", tidbClusterID)
		return whitelist, nil
	}
	return nil, err
}

func (s *Storage) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) {
	startTime := time.Now()
	operatorName := utils.GetAPIUsername(ctx)
	if !rbac.CanOperate(s.store.DB, operatorName, rbac.OtherClusterOperation) {
		return nil, false, errors.NewForbidden(s.resource, operatorName, fmt.Errorf("permission denied"))
	}
	s.lock.Lock()
	defer s.lock.Unlock()

	tidbClusterID := request.NamespaceValue(ctx)
	tc, err := s.store.GetTiDBCluster(tidbClusterID)
	if err != nil {
		return nil, false, err
	}

	id, err := getWhitelistID(name)
	if err != nil {
		return nil, false, errors.NewBadRequest("invalid name format:" + name)
	}
	obj, err := s.Get(ctx, name, nil)
	if err != nil {
		return nil, false, err
	}
	if obj.(*apiserver.Whitelist).Alias == tepWhitelistAlias {
		return nil, false, errors.NewBadRequest(fmt.Sprintf("can not delete this whitelist, %s is reserved", tepWhitelistAlias))
	}

	db, err := s.store.GetTiDBConnection(tc)
	if err == nil {
		err = utils.DoInTransaction(func(tx *sql.Tx) error {
			_, trErr := tx.Exec("delete from whitelist where id=?", id)
			if trErr != nil {
				log.Error("delete white list failed", zap.Error(trErr))
				return errors.NewInternalError(err)
			}
			//check if it's the last one
			var ipList string
			trErr = tx.QueryRow("select list from whitelist where name !=?", tepWhitelistAlias).Scan(&ipList)
			if trErr == sql.ErrNoRows {
				log.Info("no more whitelist is found, delete default tep ip")
				_, err = tx.Exec("delete from whitelist where name=?", tepWhitelistAlias)
				if err != nil {
					log.Error("delete default tep whitelist failed", zap.Error(err))
					return errors.NewInternalError(err)
				}
			}
			return nil
		}, db)

		err = s.store.AddEvent(&utils.Event{
			UserID:         operatorName,
			Payload:        fmt.Sprintf("delete white list %s", name),
			OperationType:  entity.OperateOperation,
			ManagementType: entity.InstanceManagement,
			CostTime:       time.Now().Sub(startTime).Nanoseconds(),
		})
		if err != nil {
			return nil, false, err
		}
		err = s.store.FlushPlugin(db, store.WhitelistPlugin)
		if err != nil {
			return nil, false, err
		}
		i18n.PlatformMessage.AddPlatformMessageAndLogError(s.store.DB, i18n.OperateMessageType, i18n.UpdateClusterWhitelist, "delete", tidbClusterID)
		return nil, true, nil
	}
	return nil, false, err
}

func (s *Storage) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) {
	startTime := time.Now()
	operatorName := utils.GetAPIUsername(ctx)
	if !rbac.CanOperate(s.store.DB, operatorName, rbac.OtherClusterOperation) {
		return nil, false, errors.NewForbidden(s.resource, operatorName, fmt.Errorf("permission denied"))
	}

	tidbClusterID := request.NamespaceValue(ctx)
	tc, err := s.store.GetTiDBCluster(tidbClusterID)
	if err != nil {
		return nil, false, err
	}

	id, err := getWhitelistID(name)
	if err != nil {
		return nil, false, errors.NewBadRequest("invalid name format:" + name)
	}
	obj, err := s.Get(ctx, name, nil)
	if err != nil {
		return nil, false, err
	}
	if obj.(*apiserver.Whitelist).Alias == tepWhitelistAlias {
		return nil, false, errors.NewBadRequest(fmt.Sprintf("can not update this whitelist, %s is resevred", tepWhitelistAlias))
	}
	newObj, err := objInfo.UpdatedObject(ctx, obj)
	if err != nil {
		return nil, false, err
	}
	whitelist := newObj.(*apiserver.Whitelist)
	if err := s.Validate(whitelist); err != nil {
		return nil, false, err
	}

	db, err := s.store.GetTiDBConnection(tc)
	if err == nil {
		var ipList string
		dbErr := db.QueryRow("select list from whitelist where name=? and id !=? ", whitelist.Alias, id).Scan(&ipList)
		if dbErr == nil && ipList != "" {
			return nil, false, errors.NewBadRequest("already have a whitelist named " + whitelist.Alias)
		}
		_, dbErr = db.Exec("update whitelist set list=? , name=? where id=?", strings.Join(whitelist.Ips, ","), whitelist.Alias, id)
		if dbErr != nil {
			log.Error("update white list failed", zap.Error(dbErr))
			return nil, false, errors.NewInternalError(err)
		}
		err = s.store.AddEvent(&utils.Event{
			UserID:         operatorName,
			Payload:        newObj,
			OperationType:  entity.OperateOperation,
			ManagementType: entity.InstanceManagement,
			CostTime:       time.Now().Sub(startTime).Nanoseconds(),
		})
		if err != nil {
			return nil, false, err
		}
		err = s.store.FlushPlugin(db, store.WhitelistPlugin)
		if err != nil {
			return nil, false, err
		}
		i18n.PlatformMessage.AddPlatformMessageAndLogError(s.store.DB, i18n.OperateMessageType, i18n.UpdateClusterWhitelist, "update", tidbClusterID)
		return whitelist, false, nil
	}
	return nil, false, err
}

func (s *Storage) NewList() runtime.Object {
	return &apiserver.WhitelistList{}
}

func (s *Storage) NamespaceScoped() bool {
	return true
}

func (s *Storage) NewExtraListOptions() runtime.Object {
	return &apiserver.ListOptions{}
}

func (s *Storage) Validate(whiteList *apiserver.Whitelist) error {
	if whiteList == nil {
		return errors.NewBadRequest("whitelist can not be empty")
	}
	if strings.TrimSpace(whiteList.Alias) == "" {
		return errors.NewBadRequest("whitelist name can not be empty")
	}
	if strings.TrimSpace(whiteList.Alias) == tepWhitelistAlias {
		return errors.NewBadRequest(tepWhitelistAlias + " is a reserved alias")
	}
	if !WhitelistNameRegexp.MatchString(strings.TrimSpace(whiteList.Alias)) {
		return errors.NewBadRequest(fmt.Sprintf("whitelist name is not valid, regex used for validation is %s", WhitelistNameRegexp.String()))
	}
	if len(whiteList.Ips) == 0 {
		return errors.NewBadRequest("whitelist ip set can not be empty")
	}
	for _, ip := range whiteList.Ips {
		_, _, err := net.ParseCIDR(ip)
		if err != nil {
			return errors.NewBadRequest(fmt.Sprintf("%s is not valid CIDR format: %s", ip, err.Error()))
		}
	}
	return nil
}

func getWhitelistID(name string) (int64, error) {
	idStr := strings.TrimPrefix(name, whitelistNamePrefix)
	return strconv.ParseInt(idStr, 10, 64)
}

func formatWhitelistName(id int64) string {
	return fmt.Sprintf("%s%d", whitelistNamePrefix, id)
}
