package branch_svc

import (
	"context"
	"encoding/json"
	"fmt"
	"slices"
	"strconv"
	"time"

	"github.com/tidbcloud/pkgv2/bizerr"
	"github.com/tidbcloud/pkgv2/clusterproxy"
	"github.com/tidbcloud/pkgv2/log"
	"github.com/tidbcloud/pkgv2/tracing"
	"github.com/tidbcloud/serverless-service/idl/pbgen/proto/console"
	"github.com/tidbcloud/serverless-service/internal/config"
	"github.com/tidbcloud/serverless-service/internal/domain/constant"
	"github.com/tidbcloud/serverless-service/internal/domain/facade"
	"github.com/tidbcloud/serverless-service/internal/domain/service"
	"github.com/tidbcloud/serverless-service/internal/domain/vo"
	"github.com/tidbcloud/serverless-service/internal/errorcode"
	"github.com/tidbcloud/serverless-service/internal/utils"
	"github.com/tidbcloud/tiinf/sdk/auditlog"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"go.uber.org/zap"
)

var consoleTracer = otel.Tracer("branch.console")

type branchConsoleService struct {
	bs                         service.BranchService
	billingService             service.BillingService
	infraFacade                facade.InfraFacade
	clusterProxyProviderFacade facade.ClusterProxyProviderFacade
	encryptorFacade            facade.EncryptorFacade
	console.UnimplementedBranchServiceServer
}

func NewBranchConsoleService(
	service service.BranchService,
	billingService service.BillingService,
	infraFacade facade.InfraFacade,
	clusterProxyProviderFacade facade.ClusterProxyProviderFacade,
	encryptorFacade facade.EncryptorFacade,
) console.BranchServiceServer {
	return &branchConsoleService{
		bs:                         service,
		billingService:             billingService,
		infraFacade:                infraFacade,
		clusterProxyProviderFacade: clusterProxyProviderFacade,
		encryptorFacade:            encryptorFacade,
	}
}

func (s *branchConsoleService) ListBranches(
	ctx context.Context,
	req *console.ListBranchesReq,
) (resp *console.ListBranchesResp, bizerror error) {
	start := time.Now()
	ctx, span := consoleTracer.Start(ctx, "branchConsoleService.ListBranches")
	defer func() {
		tracing.EndSpan(span, bizerror)
		CollectBranchMetrics(MetricTypeConsole, MetricMethodListBranches, start, bizerror)
		bizerror = errorcode.PrepareBranchErrorMessage(ctx, bizerror)
	}()

	span.SetAttributes(
		attribute.String("main_branch_id", req.ClusterId),
	)

	pager := utils.NewPagerNumberPager()
	bizerror = pager.Normalize(req)
	if bizerror != nil {
		return nil, bizerror
	}

	span.AddEvent("call branch_svc.ListBranches")
	opt := vo.ListBranchesOpt{
		Page:       pager.Page(),
		PageSize:   pager.PageSize(),
		SortByTime: true,
		FullBranch: true,
	}
	branches, count, bizerror := s.bs.ListBranchesInPage(ctx, req.ClusterId, opt)
	if bizerror != nil {
		return nil, bizerror
	}

	if len(branches) == 0 {
		return &console.ListBranchesResp{
			Branches: []*console.BasicBranch{},
			Total:    uint32(count),
		}, nil
	}

	span.AddEvent("call billing_svc.ListBranchStorage")
	rowStorageUsage, err := s.bs.ListBranchesRowStorage(ctx, branches)
	if err != nil {
		return nil, bizerr.New(errorcode.ErrCodeSvcO11y, errorcode.ErrMsgGetSkuUsagesFailed)
	}

	span.AddEvent("build ListBranchResp")
	brs := make([]*console.BasicBranch, 0, len(branches))
	for _, b := range branches {
		model, bizerror := b.ConvertToConsoleProtoBasic()
		if bizerror != nil {
			return nil, bizerror
		}
		model.Usages = &console.Usages{}
		// only set row storage usage when branch is running
		if b.Status == constant.BranchStatusRunning {
			model.Usages.RowStorage = int64(rowStorageUsage[strconv.FormatUint(uint64(b.KeyspaceId), 10)])
		}
		brs = append(brs, model)
	}

	return &console.ListBranchesResp{
		Branches: brs,
		Total:    uint32(count),
	}, nil
}

func (s *branchConsoleService) CreateBranch(
	ctx context.Context,
	req *console.CreateBranchReq,
) (resp *console.CreateBranchResp, bizerror error) {
	start := time.Now()
	ctx, span := consoleTracer.Start(ctx, "branchConsoleService.CreateBranch")
	defer func() {
		tracing.EndSpan(span, bizerror)
		CollectBranchMetrics(MetricTypeConsole, MetricMethodCreateBranch, start, bizerror)
		bizerror = errorcode.PrepareBranchErrorMessage(ctx, bizerror)
		//nolint:revive
		go func() {
			branchID := ""
			if resp != nil {
				branchID = resp.Id
			}
			details := auditlog.Details{
				auditlog.KeyBranchID:   branchID,
				auditlog.KeyBranchName: req.DisplayName,
			}
			submitAudit(ctx, auditlog.EventCreateBranch, req.OrgId, req.ProjectId, req.ClusterId, details, bizerror)
		}()
	}()

	parentId, err := utils.GetBranchParentId(req.ParentId, req.ClusterId)
	if err != nil {
		return nil, bizerr.New(errorcode.ErrCodeInvalidBranchParentId, "invalid parent id")
	}

	span.SetAttributes(
		attribute.String("cluster_id", req.ClusterId),
		attribute.String("branch_parent_id", parentId),
		attribute.String("display_name", req.DisplayName),
	)

	parentTimestamp := utils.ConvertTimestamp(req.ParentTimestamp)
	if parentTimestamp != nil {
		span.SetAttributes(
			attribute.String("parent_timestamp", parentTimestamp.String()),
		)
	}

	user, err := utils.GetUserFromContext(ctx)
	if err != nil {
		return nil, bizerr.New(bizerr.ErrCodeUnknown, "user not found")
	}

	span.AddEvent("call branch_svc.CreateBranch")
	branch, bizerror := s.bs.CreateBranch(
		ctx,
		req.ClusterId,
		req.DisplayName,
		parentId,
		parentTimestamp,
		"",
		user.ID,
		nil,
	)
	if bizerror != nil {
		return nil, bizerror
	}

	return &console.CreateBranchResp{
		Id: branch.ID,
	}, nil
}

func (s *branchConsoleService) GetBranch(
	ctx context.Context,
	req *console.GetBranchReq,
) (resp *console.Branch, bizerror error) {
	start := time.Now()
	ctx, span := consoleTracer.Start(ctx, "branchConsoleService.GetBranch")
	defer func() {
		tracing.EndSpan(span, bizerror)
		CollectBranchMetrics(MetricTypeConsole, MetricMethodGetBranch, start, bizerror)
		bizerror = errorcode.PrepareBranchErrorMessage(ctx, bizerror)
	}()
	span.SetAttributes(
		attribute.String("branch_root_id", req.ClusterId),
		attribute.String("branch_id", req.BranchId),
	)

	span.AddEvent("call branch_svc.GetBranch")
	b, bizerror := s.bs.GetBranchById(ctx, req.ClusterId, req.BranchId)
	if bizerror != nil {
		return nil, bizerror
	}

	resp, bizerror = b.ConvertToConsoleProtoFull()
	if bizerror != nil {
		return nil, bizerror
	}

	return resp, nil
}

func (s *branchConsoleService) DeleteBranch(
	ctx context.Context,
	req *console.DeleteBranchReq,
) (resp *console.DeleteBranchResp, bizerror error) {
	start := time.Now()
	ctx, span := consoleTracer.Start(ctx, "branchConsoleService.DeleteBranch")
	defer func() {
		tracing.EndSpan(span, bizerror)
		CollectBranchMetrics(MetricTypeConsole, MetricMethodDeleteBranch, start, bizerror)
		bizerror = errorcode.PrepareBranchErrorMessage(ctx, bizerror)
		details := auditlog.Details{
			auditlog.KeyBranchID: req.BranchId,
		}
		go submitAudit(ctx, auditlog.EventDeleteBranch, req.OrgId, req.ProjectId, req.ClusterId, details, bizerror)
	}()
	span.SetAttributes(
		attribute.String("branch_root_id", req.ClusterId),
		attribute.String("branch_id", req.BranchId),
	)

	user, err := utils.GetUserFromContext(ctx)
	if err != nil {
		return nil, bizerr.New(bizerr.ErrCodeUnknown, "user not found")
	}

	span.AddEvent("call branch_svc.DeleteBranch")
	_, bizerror = s.bs.DeleteBranchById(ctx, req.ClusterId, req.BranchId, user.ID)
	if bizerror != nil {
		return nil, bizerror
	}

	return &console.DeleteBranchResp{}, nil
}

func (s *branchConsoleService) ChangeBranchPassword(
	ctx context.Context,
	req *console.ChangeBranchPasswordReq,
) (resp *console.DefaultResp, bizerror error) {
	start := time.Now()
	ctx, span := consoleTracer.Start(ctx, "branchConsoleService.changeBranchPassword")
	defer func() {
		tracing.EndSpan(span, bizerror)
		CollectBranchMetrics(MetricTypeConsole, MetricMethodChangeBranchPassword, start, bizerror)
		bizerror = errorcode.PrepareBranchErrorMessage(ctx, bizerror)
		details := auditlog.Details{
			auditlog.KeyBranchID: req.BranchId,
		}
		go submitAudit(
			ctx,
			auditlog.EventSetBranchRootPassword,
			req.OrgId,
			req.ProjectId,
			req.ClusterId,
			details,
			bizerror,
		)
	}()

	user, err := utils.GetUserFromContext(ctx)
	if err != nil {
		return nil, bizerr.New(bizerr.ErrCodeUnknown, "user not found")
	}

	span.SetAttributes(
		attribute.String("cluster_id", req.ClusterId),
		attribute.String("branch_id", req.BranchId),
		attribute.String("user_id", fmt.Sprint(user.ID)),
	)

	span.AddEvent("get branch")
	branch, bizerror := s.bs.GetBranchById(ctx, req.ClusterId, req.GetBranchId())
	if bizerror != nil {
		return nil, bizerror
	}
	span.AddEvent("update sql user")
	root := fmt.Sprintf("%s.root", branch.UserPrefix)
	if err := s.bs.UpdateSqlUser(ctx, req.ClusterId, req.BranchId, root, req.Password); err != nil {
		return nil, err
	}

	return &console.DefaultResp{}, nil
}

func (s *branchConsoleService) UpdateBranch(
	ctx context.Context,
	req *console.UpdateBranchReq,
) (resp *console.DefaultResp, bizerror error) {
	start := time.Now()
	ctx, span := consoleTracer.Start(ctx, "branchConsoleService.updateBranch")
	defer func() {
		tracing.EndSpan(span, bizerror)
		CollectBranchMetrics(MetricTypeConsole, MetricMethodUpdateBranch, start, bizerror)
		bizerror = errorcode.PrepareBranchErrorMessage(ctx, bizerror)
		details := auditlog.Details{
			auditlog.KeyBranchID:      req.BranchId,
			auditlog.KeyBranchNewName: req.Branch.DisplayName,
		}
		go submitAudit(ctx, auditlog.EventUpdateBranch, req.OrgId, req.ProjectId, req.ClusterId, details, bizerror)
	}()

	user, err := utils.GetUserFromContext(ctx)
	if err != nil {
		return nil, bizerr.New(bizerr.ErrCodeUnknown, "user not found")
	}

	span.SetAttributes(
		attribute.String("cluster_id", req.ClusterId),
		attribute.String("branch_id", req.BranchId),
		attribute.String("display_name", req.Branch.DisplayName),
		attribute.String("user_id", fmt.Sprint(user.ID)),
	)

	span.AddEvent("call branch_svc.UpdateBranch")
	branch := &vo.UpdateBranchReq{
		DisplayName: req.Branch.DisplayName,
	}
	_, bizerror = s.bs.UpdateBranch(ctx, req.ClusterId, req.BranchId, branch)
	if bizerror != nil {
		return nil, bizerror
	}

	return &console.DefaultResp{}, nil
}

func (s *branchConsoleService) ListDatabases(
	ctx context.Context,
	req *console.BranchListDatabasesReq,
) (resp *console.BranchListDatabasesResp, bizerror error) {
	start := time.Now()
	ctx, span := consoleTracer.Start(ctx, "branchConsoleService.ListDatabases")
	defer func() {
		tracing.EndSpan(span, bizerror)
		CollectBranchMetrics(MetricTypeConsole, MetricMethodListDatabases, start, bizerror)
		bizerror = errorcode.PrepareBranchErrorMessage(ctx, bizerror)
	}()

	user, err := utils.GetUserFromContext(ctx)
	if err != nil {
		return nil, bizerr.New(bizerr.ErrCodeUnknown, "user not found")
	}

	span.SetAttributes(
		attribute.String("cluster_id", req.ClusterId),
		attribute.String("branch_id", req.BranchId),
		attribute.String("user_id", fmt.Sprint(user.ID)),
	)

	logger := log.WithContextL(ctx).With(zap.String("cluster_id", req.ClusterId),
		zap.String("branch_id", req.BranchId), zap.Uint64("user_id", user.ID))

	span.AddEvent("get branch")
	branch, bizerror := s.bs.GetBranchById(ctx, req.ClusterId, req.BranchId)
	if bizerror != nil {
		logger.Error("get branch failed", zap.Error(bizerror))
		return nil, bizerror
	}
	if branch.Status != constant.BranchStatusRunning {
		return nil, bizerr.New(errorcode.ErrCodeBranchNotRunning, "")
	}

	span.AddEvent("load branch cr")
	branchCr, err := s.infraFacade.GetBranch(ctx, branch.InfraProjectId, branch.ID)
	if err != nil {
		logger.Error("get branch cr failed", zap.Error(err))
		return nil, bizerr.New(errorcode.ErrCodeSvcInfra, "")
	}

	logger = logger.With(zap.String("region", branch.RegionName))
	span.AddEvent("get cluster proxy")
	cloudAdminPwd, err := s.encryptorFacade.DecryptBase64(branch.EncryptedCloudAdminPwd)
	if err != nil {
		logger.Error("encryptorFacade decrypt failed", zap.Error(err))
		return nil, bizerr.New(errorcode.ErrCodeSvcEncryptor, "")
	}
	operator := &clusterproxy.ClusterOperator{
		Username:   constant.CloudAdminUsername,
		AuthMethod: clusterproxy.AuthMethodPassword,
		Credential: cloudAdminPwd,
	}
	proxy, err := s.clusterProxyProviderFacade.GetBranchProxy(ctx, branchCr, operator)
	if err != nil {
		logger.Error("get cluster proxy failed", zap.Error(err))
		return nil, bizerr.New(errorcode.ErrCodeSvcClusterProxy, "")
	}

	span.AddEvent("show database sql")
	res, err := proxy.QuerySQL(ctx, "SHOW DATABASES")
	if err != nil {
		logger.Error("failed to query sql", zap.Error(err))
		return nil, err
	}

	type DatabaseResult struct {
		Databases [][]byte `json:"Database"`
	}
	result := DatabaseResult{}
	err = json.Unmarshal(res, &result)
	if err != nil {
		logger.Error("failed to unmarshal json", zap.Error(err))
		return nil, bizerr.New(bizerr.ErrCodeJsonMarshal, "")
	}
	var databases []string
	for _, database := range result.Databases {
		if !slices.Contains(constant.ExcludeDatabases, string(database)) {
			databases = append(databases, string(database))
		}
	}

	return &console.BranchListDatabasesResp{
		Databases: databases,
	}, nil
}

func submitAudit(
	ctx context.Context,
	event auditlog.EventType,
	orgID, projectID uint64,
	clusterID string,
	details auditlog.Details,
	err error,
) {
	logger := log.WithContextL(ctx)
	if config.Global().ServerConfig.EnableAuditLog {
		/* set web console auditlog */
		clusterIDUint64, e := strconv.ParseUint(clusterID, 10, 64)
		if e != nil {
			logger.Error("parse clusterID failed when submit audit log", zap.Error(e))
		}
		eventBuilder := auditlog.NewEvent(ctx, event).
			SetResult(err == nil).
			SetProjectID(projectID).
			SetOrgID(orgID).
			SetClusterID(clusterIDUint64).
			SetDetails(details.Build())
		if e := eventBuilder.Submit(); e != nil {
			logger.Error("submit audit log failed", zap.Error(e))
		}
	}
}

func (s *branchConsoleService) ResetBranch(
	ctx context.Context,
	req *console.ResetBranchReq,
) (resp *console.DefaultResp, bizerror error) {
	start := time.Now()
	ctx, span := consoleTracer.Start(ctx, "branchConsoleService.resetBranch")
	defer func() {
		tracing.EndSpan(span, bizerror)
		CollectBranchMetrics(MetricTypeConsole, MetricMethodResetBranch, start, bizerror)
		bizerror = errorcode.PrepareBranchErrorMessage(ctx, bizerror)
		details := auditlog.Details{
			auditlog.KeyBranchID: req.BranchId,
		}
		go submitAudit(ctx, auditlog.EventResetBranch, req.OrgId, req.ProjectId, req.ClusterId, details, bizerror)
	}()

	user, err := utils.GetUserFromContext(ctx)
	if err != nil {
		return nil, bizerr.New(bizerr.ErrCodeUnknown, "user not found")
	}

	span.SetAttributes(
		attribute.String("cluster_id", req.ClusterId),
		attribute.String("branch_id", req.BranchId),
		attribute.String("user_id", fmt.Sprint(user.ID)),
	)

	span.AddEvent("call branch_svc.ResetBranch")
	_, err = s.bs.ResetBranchById(ctx, req.ClusterId, req.BranchId)
	if err != nil {
		return nil, err
	}

	return &console.DefaultResp{}, nil
}
