package auditsetting

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/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"
	"strconv"
	"strings"
	"time"
)

const (
	dbALL    = ""
	dbDQL    = "Select"
	dbDML    = "Insert,Update,Delete,LoadData,Replace"
	dbDDL    = "AlterTable,CreateDatabase,CreateIndex,CreateTable,CreateView,DropDatabase,DropIndex,DropTable,TruncateTable,CreateUser,CreateBinding"
	dbDCL    = "Commit,RollBack,Begin,Grant,Flush,Set"
	dbOthers = "Use,Prepare,Execute,Deallocate,Revoke,Show,Explain,Change,AnalyzeTable"

	AccessTypeAll    string = "All"
	AccessTypeDQL    string = "DQL"
	AccessTypeDML    string = "DML"
	AccessTypeDDL    string = "DDL"
	AccessTypeDCL    string = "DCL"
	AccessTypeOthers string = "Others"

	AuditSettingNamePrefix string = "audit-setting-"
)

var tepType2AccessTypeMap = map[string]string{
	AccessTypeAll:    dbALL,
	AccessTypeDQL:    dbDQL,
	AccessTypeDML:    dbDML,
	AccessTypeDDL:    dbDDL,
	AccessTypeDCL:    dbDCL,
	AccessTypeOthers: dbOthers,
}

var dbAccessType2TEPTypeMap = map[string]string{}

func init() {
	for key, value := range tepType2AccessTypeMap {
		for _, item := range strings.Split(value, ",") {
			dbAccessType2TEPTypeMap[strings.TrimSpace(item)] = key
		}
	}
}

type Storage struct {
	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 "TiDBAuditSetting"
}

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

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
	}

	settingID, err := getAuditSettingID(name)
	if err != nil {
		return nil, errors.NewBadRequest(fmt.Sprintf("invalid auditseting name: %s", name))
	}
	db, err := s.store.GetTiDBConnection(tc)
	if err == nil {
		var user, dbName, table, accessType string
		dbErr := db.QueryRow("select user, db, tbl,access_type from tidb_audit_table_access where id=?", settingID).Scan(&user, &dbName, &table, &accessType)
		if dbErr == sql.ErrNoRows {
			return nil, errors.NewNotFound(s.resource, fmt.Sprintf("%d", settingID))
		}

		return &apiserver.TiDBAuditSetting{ObjectMeta: metav1.ObjectMeta{Name: name, UID: types.UID(name)}, User: user, Database: dbName, Table: table, AccessTypes: getTEPAccessType(accessType)}, 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)
	list := &apiserver.TiDBAuditSettingList{}
	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, user, db, tbl, access_type from tidb_audit_table_access limit %d,%d ", startIndex, size))
			if err != nil {
				log.Error("query audit setting list failed", zap.Error(err))
				return errors.NewInternalError(err)
			}
			defer utils.CallDeferMethod(rows.Close, fmt.Sprintf("close rows failed, instance=%s", tidbClusterID))
			var user, database, tbl, accessType string
			var id int64
			for rows.Next() {
				if err = rows.Scan(&id, &user, &database, &tbl, &accessType); err == nil {
					list.Items = append(list.Items, apiserver.TiDBAuditSetting{ObjectMeta: metav1.ObjectMeta{Name: formatAuditSettingName(id)}, User: user, Database: database, Table: tbl, AccessTypes: getTEPAccessType(accessType)})
				}
			}
			var total int64
			if err := tx.QueryRow("SELECT count(*) from tidb_audit_table_access").Scan(&total); err != nil {
				log.Error("query audit setting total failed", zap.Error(err))
				return errors.NewInternalError(err)
			}
			list.Total = total
			return nil
		}, db); err != nil {
			return nil, err
		}
		return list, nil
	}
	return nil, 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"))
	}

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

	setting := obj.(*apiserver.TiDBAuditSetting)
	if err := s.validate(setting); len(err) > 0 {
		return nil, errors.NewBadRequest(err)
	}

	db, err := s.store.GetTiDBConnection(tc)
	if err == nil {
		dbTypes, err := toDBAccessType(setting.AccessTypes)
		if err != nil {
			return nil, err
		}
		result, err := db.Exec("insert into tidb_audit_table_access(user, db, tbl,access_type) values(?,?,?,?)", setting.User, setting.Database, setting.Table, dbTypes)
		if err != nil {
			log.Error("add audit setting failed", zap.Error(err))
			return nil, errors.NewInternalError(fmt.Errorf("add audit setting failed, error: %v", err))
		}
		apiErr := s.store.AddEvent(&utils.Event{
			UserID:         operatorName,
			Payload:        setting,
			OperationType:  entity.OperateOperation,
			ManagementType: entity.InstanceManagement,
			CostTime:       time.Now().Sub(startTime).Nanoseconds(),
		})
		if apiErr != nil {
			return nil, apiErr
		}
		lastInsertID, _ := result.LastInsertId()
		setting.Name = formatAuditSettingName(lastInsertID)
		err = s.store.FlushPlugin(db, store.AuditPlugin)
		if err != nil {
			return nil, err
		}
		return setting, 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"))
	}

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

	db, err := s.store.GetTiDBConnection(tc)
	if err == nil {
		settingID, err := getAuditSettingID(name)
		if err != nil {
			return nil, false, errors.NewBadRequest(fmt.Sprintf("invalid name: %s", name))
		}
		_, dbErr := db.Exec("delete from tidb_audit_table_access where id=?", settingID)
		if dbErr != nil {
			log.Error("delete audit setting  failed", zap.Error(dbErr))
			return nil, false, errors.NewInternalError(fmt.Errorf("delete audit setting failed"))
		}
		utils.AddEventAndLogError(s.store.DB, &utils.Event{
			UserID:         operatorName,
			Payload:        name,
			OperationType:  entity.OperateOperation,
			ManagementType: entity.InstanceManagement,
			CostTime:       time.Now().Sub(startTime).Nanoseconds(),
		})
		err = s.store.FlushPlugin(db, store.AuditPlugin)
		if err != nil {
			return nil, true, err
		}
		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
	}

	obj, err := s.Get(ctx, name, nil)
	if err != nil {
		return nil, false, err
	}
	newObj, err := objInfo.UpdatedObject(ctx, obj)
	if err != nil {
		return nil, false, err
	}
	setting := newObj.(*apiserver.TiDBAuditSetting)
	if err := s.validate(setting); len(err) > 0 {
		return nil, false, errors.NewBadRequest(err)
	}
	db, err := s.store.GetTiDBConnection(tc)
	if err == nil {
		dbTypes, err := toDBAccessType(setting.AccessTypes)
		if err != nil {
			return nil, false, err
		}
		settingID, err := getAuditSettingID(name)
		if err != nil {
			return nil, false, errors.NewBadRequest(fmt.Sprintf("invalid name: %s", name))
		}
		_, dbErr := db.Exec("update tidb_audit_table_access set user=?, db=?, tbl=?, access_type=? where id=?", setting.User, setting.Database, setting.Table, dbTypes, settingID)
		if dbErr != nil {
			log.Error("update audit setting  failed", zap.Error(dbErr))
			return nil, false, errors.NewInternalError(fmt.Errorf("update audit setting failed"))
		}
		utils.AddEventAndLogError(s.store.DB, &utils.Event{
			UserID:         operatorName,
			Payload:        name,
			OperationType:  entity.OperateOperation,
			ManagementType: entity.InstanceManagement,
			CostTime:       time.Now().Sub(startTime).Nanoseconds(),
		})
		err = s.store.FlushPlugin(db, store.AuditPlugin)
		if err != nil {
			return nil, false, err
		}
		return newObj, false, nil
	}
	return nil, false, err
}

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

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

func getTEPAccessType(dbAccessType string) []string {
	var set = map[string]struct{}{}
	setToSlice := func(set map[string]struct{}) (slice []string) {
		slice = make([]string, 0, len(set))
		for val := range set {
			slice = append(slice, val)
		}
		return
	}
	for _, item := range strings.Split(dbAccessType, ",") {
		if item == "" {
			return []string{AccessTypeAll}
		}
		if tepType, found := dbAccessType2TEPTypeMap[strings.TrimSpace(item)]; found {
			set[tepType] = struct{}{}
		}
	}
	return setToSlice(set)
}

func toDBAccessType(tepTypes []string) (string, error) {
	var str string
	for _, tepType := range tepTypes {
		if tepType == AccessTypeAll {
			return "", nil
		}
		if dbType, found := tepType2AccessTypeMap[tepType]; found {
			str += "," + dbType
		} else {
			return "", errors.NewBadRequest("unknown access type")
		}
	}
	if strings.HasPrefix(str, ",") {
		str = strings.TrimPrefix(str, ",")
	}
	return str, nil
}

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

func (s *Storage) validate(setting *apiserver.TiDBAuditSetting) string {
	if len(setting.AccessTypes) == 0 {
		return "missing access types"
	}
	return ""
}

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

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