package open

import (
	"context"
	"time"

	"github.com/elliotchance/pie/v2"
	appconfig "github.com/tidbcloud/dataflow-service/app/config"
	"github.com/tidbcloud/dataflow-service/app/models/backup"
	"github.com/tidbcloud/dataflow-service/app/models/domain"
	"github.com/tidbcloud/dataflow-service/app/models/restore"
	"github.com/tidbcloud/dataflow-service/idl/pbgen/proto/dataflow"
	"github.com/tidbcloud/dataflow-service/pkg/account"
	bizerr_wrapper "github.com/tidbcloud/dataflow-service/pkg/bizerr-wrapper"
	"github.com/tidbcloud/dataflow-service/pkg/grpc/grpcli"
	"github.com/tidbcloud/dataflow-service/pkg/log"
	"github.com/tidbcloud/idl/pbgen/proto/openapi/cm"
	"github.com/tidbcloud/idl/pbgen/proto/openapi/dp"
	"github.com/tidbcloud/pkgv2/bizerr"
	"github.com/tidbcloud/pkgv2/db"
	mgmt "github.com/tidbcloud/tidb-management-service/api/spec/tidb_mgmt_service/v1"
	"go.uber.org/zap"
	"google.golang.org/protobuf/types/known/timestamppb"
)

type RestoreController interface {
	dp.RestoreServer
}

type restoreControllerImpl struct {
	dp.UnimplementedRestoreServer
	BaseController

	restoreRouter       restore.RestoreModel
	snapshotBackupModel backup.SnapshotBackupModel

	mgmtCli grpcli.MgmtSvcGrpcClient
}

func NewRestoreController(
	trans db.TransProvider,
	restoreRouter restore.RestoreModel,
	snapshotBackupModel backup.SnapshotBackupModel,
	mgmtCli grpcli.MgmtSvcGrpcClient,
) RestoreController {
	return &restoreControllerImpl{
		BaseController:      BaseController{trans: trans},
		restoreRouter:       restoreRouter,
		snapshotBackupModel: snapshotBackupModel,
		mgmtCli:             mgmtCli,
	}
}

func (c *restoreControllerImpl) ListRestoreOfProject(
	ctx context.Context, req *dp.ListRestoreOfProjectReq,
) (*dp.ListRestoreOfProjectResp, error) {
	ctx = log.WithTraceID(ctx)
	logger := log.GetLogger(ctx)
	err := c.checkListRestoreOfProject(ctx, req)
	if err != nil {
		return nil, err
	}

	listResp, err := c.restoreRouter.List(ctx, &domain.ListRestoreReqV2{
		Filter: &domain.ListRestoreFilter{
			ProjectIDs: []uint64{req.ProjectId},
		},
		Pager: domain.NewPager(domain.PageSizeOp(int64(req.PageSize)),
			domain.PageNumberOp(int64(req.Page))),
	})
	if err != nil {
		logger.Error("list restore failed", zap.Error(err))
		return nil, err
	}

	return c.convertToListRestoreResp(ctx, listResp)
}

func (c *restoreControllerImpl) convertToListRestoreResp(
	ctx context.Context, listResp *domain.ListRestoreRespV2,
) (*dp.ListRestoreOfProjectResp, bizerr.BizError) {
	resp := &dp.ListRestoreOfProjectResp{
		Total: uint32(listResp.Total),
	}
	if listResp.Total == 0 {
		return resp, nil
	}

	clusterMap, err := c.queryRestoringClusters(ctx, listResp)
	if err != nil {
		return nil, err
	}

	for _, item := range listResp.Items {
		newItem := &dp.ListRestoreRespItem{
			Id:              item.Id,
			CreateTimestamp: timestamppb.New(time.Unix(item.CreateTime.Unix(), 0)),
			BackupId:        item.BackupId,
			ClusterId:       item.BackupClusterId,
			Status:          RestoreStatus2OpenAPIList(item.Status),
			Cluster: &dp.ClusterInfoOfRestore{
				Id: item.RestoreClusterID,
			},
		}
		if cluster, ok := clusterMap[item.RestoreClusterID]; ok {
			newItem.Cluster.Name = cluster.GetClusterName()
			if dc := cluster.GetDedicatedCluster(); dc != nil {
				newItem.Cluster.Status = DedicatedClusterState2OpenAPIList(dc.GetState())
			}
		}
		if dp.ListRestoreRespItem_FAILED == newItem.Status {
			newItem.ErrorMessage = "please touch with support."
		}
		resp.Items = append(resp.Items, newItem)
	}
	return resp, nil
}

func (c *restoreControllerImpl) queryRestoringClusters(
	ctx context.Context, listResp *domain.ListRestoreRespV2,
) (map[uint64]*mgmt.Cluster, bizerr.BizError) {
	logger := log.GetLogger(ctx)
	clusterIDs := pie.Map(pie.Filter(listResp.Items, func(t domain.ListRestoreRespV2Item) bool {
		return t.RestoreClusterID != 0
	}), func(t domain.ListRestoreRespV2Item) uint64 {
		return t.RestoreClusterID
	})
	if len(clusterIDs) == 0 {
		return nil, nil
	}
	grpcResp, err := c.mgmtCli.ListClusters(ctx, &mgmt.ListClustersRequest{
		Filter: &mgmt.ClusterFilter{
			ClusterIds:  clusterIDs,
			View:        mgmt.ClusterFilter_CLUSTER_VIEW_BASIC,
			ClusterType: mgmt.DeployType_DEPLOY_TYPE_DEDICATED.Enum(),
		},
	})
	if err != nil {
		logger.Error("list cluster failed", zap.Error(err))
		return nil, bizerr_wrapper.NewInternalErr(appconfig.ErrInternalError)
	}
	clusterMap := make(map[uint64]*mgmt.Cluster, len(grpcResp.GetClusters()))
	for _, cluster := range grpcResp.GetClusters() {
		clusterMap[cluster.GetClusterId()] = cluster
	}
	return clusterMap, nil
}

func (c *restoreControllerImpl) checkListRestoreOfProject(
	ctx context.Context, req *dp.ListRestoreOfProjectReq,
) bizerr.BizError {
	var err bizerr.BizError
	req.Page, err = c.CheckAndSetPageDefaultValue(req.Page)
	if err != nil {
		return err
	}
	req.PageSize, err = c.CheckAndSetPageSizeDefaultValue(req.PageSize)
	if err != nil {
		return err
	}
	err = c.CheckProjectID(ctx, req.ProjectId)
	if err != nil {
		return err
	}
	return nil
}

func (c *restoreControllerImpl) GetRestore(ctx context.Context, req *dp.GetRestoreReq) (*dp.GetRestoreResp, error) {
	ctx = log.WithTraceID(ctx)
	err := c.checkGetRestore(ctx, req)
	if err != nil {
		return nil, err
	}
	user := account.GetUserFromContext(ctx)
	queryResp, err := c.restoreRouter.Query(ctx, &domain.QueryRestoreReqV2{
		Filter: &domain.ListRestoreFilter{
			TenantIDs:  []uint64{user.TenantID},
			ProjectIDs: []uint64{req.ProjectId},
			RestoreIDs: []uint64{req.RestoreId},
		},
	})
	if err != nil {
		return nil, err
	}
	return c.convertGetRestoreResp(ctx, queryResp)
}

func (c *restoreControllerImpl) convertGetRestoreResp(
	ctx context.Context, queryResp *domain.QueryRestoreRespV2,
) (*dp.GetRestoreResp, bizerr.BizError) {
	logger := log.GetLogger(ctx)
	resp := &dp.GetRestoreResp{
		Id:              queryResp.Id,
		CreateTimestamp: timestamppb.New(time.Unix(queryResp.CreateTime.Unix(), 0)),
		BackupId:        queryResp.BackupId,
		ClusterId:       queryResp.BackupClusterId,
		Status:          RestoreStatus2OpenAPIGet(queryResp.Status),
	}
	if dp.GetRestoreResp_FAILED == resp.Status {
		resp.ErrorMessage = "please touch with support."
	}
	if queryResp.RestoreClusterID != 0 {
		cluster, err := c.mgmtCli.GetAnyDedicatedClusterBasicByID(ctx, queryResp.RestoreClusterID)
		if err != nil {
			logger.Error("get cluster failed", zap.Error(err))
			return nil, bizerr_wrapper.NewInternalErr(appconfig.ErrInternalError)
		}
		resp.Cluster = &dp.ClusterInfoOfRestore{
			Id:   queryResp.RestoreClusterID,
			Name: cluster.GetClusterName(),
		}
		if dc := cluster.GetDedicatedCluster(); dc != nil {
			resp.Cluster.Status = DedicatedClusterState2OpenAPIList(dc.GetState())
		}
	}
	return resp, nil
}

func (c *restoreControllerImpl) checkGetRestore(ctx context.Context, req *dp.GetRestoreReq) bizerr.BizError {
	return c.CheckProjectID(ctx, req.ProjectId)
}

func (c *restoreControllerImpl) CreateRestore(
	ctx context.Context, req *dp.CreateRestoreReq,
) (*dp.CreateRestoreResp, error) {
	ctx = log.WithTraceID(ctx)
	logger := log.GetLogger(ctx)

	queryBackupResp, err := c.snapshotBackupModel.Query(ctx, &domain.QueryBackupReq{
		Filter: &domain.ListBackupFilter{
			BackupIDs: []uint64{req.BackupId},
		},
	})
	if err != nil {
		return nil, err
	}
	cluster, err := c.mgmtCli.GetAnyDedicatedClusterBasicByID(ctx, queryBackupResp.ClusterID)
	if err != nil {
		logger.Error("get cluster failed", zap.Error(err))
		return nil, bizerr_wrapper.NewInternalErr(appconfig.ErrInternalError)
	}

	user := account.GetUserFromContext(ctx)
	createRestoreReq := &domain.CreateRestoreReqV2{
		Mode:     domain.RestoreModeSnapshot,
		BackupID: req.BackupId,
		ClusterConfig: domain.ClusterConfig{
			Region:       cluster.GetRegion(),
			Name:         req.Name,
			RootPassword: req.Config.RootPassword,
			Port:         req.Config.Port,
			Components:   c.convertClusterComponents(req.Config.Components),
			IpAccessList: c.convertClusterTrafficFilter(req.Config.IpAccessList),
		},
		UserID:    user.ID,
		ProjectID: req.ProjectId,
		TenantID:  user.TenantID,
	}
	createResp, err := c.restoreRouter.Create(ctx, createRestoreReq)
	if err != nil {
		return nil, err
	}
	logger.Info("create restore success",
		zap.Uint64("restoreID", createResp.ID), zap.Any("workflowID", createResp.WorkflowID))
	return &dp.CreateRestoreResp{
		Id:        createResp.ID,
		ClusterId: createResp.ClusterID,
	}, nil
}

func (c *restoreControllerImpl) convertClusterTrafficFilter(filters []*cm.IpAccessListItem) []domain.IpAccessListItem {
	if len(filters) == 0 {
		return nil
	}
	return pie.Map(filters, func(t *cm.IpAccessListItem) domain.IpAccessListItem {
		return domain.IpAccessListItem{
			Cidr:        t.Cidr,
			Description: t.Description,
		}
	})
}

func (c *restoreControllerImpl) convertClusterComponents(components *cm.ClusterComponents) *domain.ClusterComponents {
	// NOTE: restore via OpenAPI is not supported for TiProxy

	clusterComponents := &domain.ClusterComponents{}
	if tidb := components.GetTidb(); tidb != nil {
		clusterComponents.Tidb = &domain.TiDBComponent{
			NodeSize:     tidb.NodeSize,
			NodeQuantity: tidb.NodeQuantity,
		}
	}

	if tikv := components.GetTikv(); tikv != nil {
		clusterComponents.Tikv = &domain.TiKVComponent{
			NodeSize:     tikv.NodeSize,
			NodeQuantity: tikv.NodeQuantity,
			// leave it empty and the mgmt-service will use pick a default value
			StorageType: dataflow.SimpleClusterComponent_STORAGE_TYPE_UNSPECIFIED,
			// ditto
			RaftIops:       0,
			StorageSizeGib: int64(tikv.StorageSizeGib),
		}
	}

	if tiflash := components.GetTiflash(); tiflash != nil {
		clusterComponents.Tiflash = &domain.TiFlashComponent{
			NodeSize:     tiflash.NodeSize,
			NodeQuantity: tiflash.NodeQuantity,
			// leave it empty and the mgmt-service will use pick a default value
			StorageType:    dataflow.SimpleClusterComponent_STORAGE_TYPE_UNSPECIFIED,
			StorageSizeGib: int64(tiflash.StorageSizeGib),
		}
	}
	return clusterComponents
}
