package plugin

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"
	"time"
)

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

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

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 {
		var plugin apiserver.TiDBPlugin
		dbErr := db.QueryRow("show plugins where name=?", name).Scan(&plugin.Name, &plugin.Status, &plugin.Type, &plugin.Library, &plugin.Licence, &plugin.Version)
		if dbErr == sql.ErrNoRows {
			return nil, errors.NewNotFound(s.resource, fmt.Sprintf("plugin %s is not found", name))
		}
		plugin.UID = types.UID(plugin.Name)
		return &plugin, 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.TiDBPluginList{Items: []apiserver.TiDBPlugin{}}
	if err == nil {
		rows, err := db.Query("show plugins")
		if err != nil {
			log.Error("query plugin list failed", zap.Error(err))
			return nil, errors.NewInternalError(err)
		}
		defer utils.CallDeferMethod(rows.Close, "close rows failed")
		items := make([]apiserver.TiDBPlugin, 0)
		for rows.Next() {
			var plugin apiserver.TiDBPlugin
			if err = rows.Scan(&plugin.Name, &plugin.Status, &plugin.Type, &plugin.Library, &plugin.Licence, &plugin.Version); err == nil {
				items = append(items, plugin)
			}
		}
		lists.Items = items
	}
	return lists, 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
	}
	if obj == nil {
		return nil, false, errors.NewNotFound(s.resource, fmt.Sprintf("plugin %s is not found", name))
	}
	newObj, err := objInfo.UpdatedObject(ctx, obj)
	plugin := newObj.(*apiserver.TiDBPlugin)
	if err != nil {
		return nil, false, err
	}
	db, err := s.store.GetTiDBConnection(tc)
	if err == nil {
		var status = "enable"
		if plugin.Status == "disable" {
			status = "disable"
		}
		_, dbErr := db.Exec(fmt.Sprintf("admin plugins %s %s", status, plugin.Name))
		if dbErr != nil {
			log.Error("change plugin status failed", zap.String("instance", tidbClusterID), zap.String("plugin", plugin.Name), zap.String("status", plugin.Status), zap.Error(dbErr))
			return nil, false, errors.NewInternalError(fmt.Errorf("unable to change plugin(%s) status", plugin.Name))
		}
		return plugin, true, s.store.AddEvent(&utils.Event{
			UserID:         operatorName,
			Payload:        fmt.Sprintf("%s %s plugin %s", tidbClusterID, plugin.Status, plugin.Name),
			OperationType:  entity.OperateOperation,
			ManagementType: entity.InstanceManagement,
			CostTime:       time.Now().Sub(startTime).Nanoseconds(),
		})
	}
	return nil, false, err
}

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

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

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