package tenant

import (
	"context"
	"fmt"
	"sort"
	"strconv"
	"sync"
	"time"

	"github.com/go-logr/logr"
	promapi "github.com/prometheus/client_golang/api"
	promv1 "github.com/prometheus/client_golang/api/prometheus/v1"
	prommodel "github.com/prometheus/common/model"
	"github.com/tidbcloud/pkgv2/db"
	"go.uber.org/dig"

	auditent "github.com/tidbcloud/devops/entity/audit"
	"github.com/tidbcloud/devops/entity/event"
	tntent "github.com/tidbcloud/devops/entity/tenant"
	"github.com/tidbcloud/devops/pkg/infra/clients/accountcli"
	"github.com/tidbcloud/devops/pkg/infra/clients/billingcli"
	"github.com/tidbcloud/devops/pkg/infra/clients/googlecli"
	"github.com/tidbcloud/devops/pkg/infra/config"
	"github.com/tidbcloud/devops/pkg/infra/consts"
	"github.com/tidbcloud/devops/pkg/infra/eventbus"
	"github.com/tidbcloud/devops/pkg/infra/log"
	"github.com/tidbcloud/devops/pkg/infra/utils/pointer"
	"github.com/tidbcloud/devops/pkg/infra/utils/repoopt"
	"github.com/tidbcloud/devops/pkg/service"
	adsvc "github.com/tidbcloud/devops/pkg/service/audit"
	"github.com/tidbcloud/devops/pkg/service/tenant/cron"
	"github.com/tidbcloud/devops/pkg/service/tenant/cron/enricher"
	"github.com/tidbcloud/devops/pkg/service/tenant/repo"
)

const (
	dbaasTenantEventGroupKey     = "Account"
	dbaasTenantPlanEventGroupKey = "sync_plan"
)

type ServiceParam struct {
	dig.In

	Conf         *config.RootConfig
	Logger       logr.Logger
	DBProvider   db.TransProvider
	AccountCli   accountcli.Client
	BillingCli   billingcli.Client
	EventBus     *eventbus.EventBus
	GoogleCli    googlecli.GoogleClient
	AuditService adsvc.AuditServiceAPI
}

type ServiceResult struct {
	dig.Out

	API     TenantServiceAPI
	Service service.Service `group:"service"`
}

type svc struct {
	*service.JobService

	conf       config.TenantServiceConfig
	accountCli accountcli.Client
	eventBus   *eventbus.EventBus
	tenantRepo repo.TenantRepo
	tenantSync cron.SyncTenantsCron
	auditSvc   adsvc.AuditServiceAPI
}

func NewService(p ServiceParam) (ServiceResult, error) {
	s := &svc{
		JobService: service.NewJobService("TenantService"),

		conf:       p.Conf.Services.Tenant,
		accountCli: p.AccountCli,
		eventBus:   p.EventBus,
		tenantRepo: repo.NewTenantRepo(p.DBProvider),
		auditSvc:   p.AuditService,
	}

	var tenantEnrichers []enricher.Enricher
	if consts.GetPartition() == consts.PartitionCN {
		tenantEnrichers = []enricher.Enricher{
			enricher.NewBillingEnricher(p.BillingCli),
		}
	} else {
		tenantEnrichers = []enricher.Enricher{
			enricher.NewSalesforceEnricher(
				p.GoogleCli.BigQuery(),
				p.AccountCli,
				s.conf.TenantTableName,
				s.conf.TestInfraTenantIDs,
			),
			enricher.NewBillingEnricher(p.BillingCli),
		}
	}
	s.tenantSync = cron.NewSyncTenantsCron(
		s.tenantRepo,
		s.accountCli,
		tenantEnrichers,
	)
	if !p.Conf.Services.Tenant.CronDisable {
		s.AddJob(service.MustNewCronJob("SyncTenants", s.conf.CronSpec, s.tenantSync.SyncTenants))
	}

	s.eventBus.DBaasTenantEvent.Subscribe(dbaasTenantEventGroupKey, s.HandleDBaasTenantEvent)
	s.eventBus.DBaasTenantPlanEvent.Subscribe(dbaasTenantPlanEventGroupKey, s.HandleDBaasTenantPlanEvent)
	return ServiceResult{API: s, Service: s}, nil
}

func (s *svc) ListTenants(ctx context.Context, param ListTenantsParam) ([]*tntent.Tenant, int64, error) {
	opts := []repoopt.Option{}
	if len(param.Filter.ID) > 0 {
		opts = append(opts, repo.InIds(param.Filter.ID))
	}
	if len(param.Filter.Status) > 0 {
		opts = append(opts, repo.InStatus(param.Filter.Status))
	}
	if len(param.Filter.Kind) > 0 {
		opts = append(opts, repo.InKind(param.Filter.Kind))
	}
	if len(param.Filter.SalesOwnerEmail) > 0 {
		opts = append(opts, repo.InSalesOwnerEmail(param.Filter.SalesOwnerEmail))
	}
	if len(param.Filter.Plan) > 0 {
		opts = append(opts, repo.InPlan(param.Filter.Plan))
	}
	if len(param.Filter.Region) > 0 {
		opts = append(opts, repo.InRegion(param.Filter.Region))
	}
	if len(param.Filter.Segmentation) > 0 {
		opts = append(opts, repo.InSegmentation(param.Filter.Segmentation))
	}
	if param.Search != "" {
		opts = append(opts, repo.Search(param.Search))
	}
	total, err := s.tenantRepo.Count(ctx, opts...)
	if err != nil {
		return nil, 0, err
	}

	opts = append(opts, repo.WithPager(param.Page, param.PerPage))
	if param.SortBy != "" && param.Order != "" {
		desc := false
		if param.Order == OrderTypeDESC {
			desc = true
		}
		opts = append(opts, repo.WithOrder(repo.OrderColumn(param.SortBy), desc))
	} else {
		opts = append(opts, repo.DefaultOrder())
	}
	tenants, err := s.tenantRepo.Query(ctx, opts...)
	if err != nil {
		return nil, 0, err
	}
	return tenants, total, nil
}

func (s *svc) GetTenant(ctx context.Context, id uint64) (*tntent.Tenant, error) {
	tenant, err := s.tenantRepo.Load(ctx, id)
	if err != nil {
		return nil, err
	}
	return tenant, nil
}

func (s *svc) UpdateTenant(ctx context.Context, req *accountcli.UpdateTenantReq, tenantID, applicant string) error {
	if err := s.accountCli.UpdateTenant(ctx, req, tenantID, applicant); err != nil {
		return fmt.Errorf("update tenant from account service failed: %s", err)
	}

	return nil
}

func (s *svc) DeleteTenant(ctx context.Context, id uint64, applicant string) error {
	if err := s.accountCli.DeleteTenant(ctx, id, applicant); err != nil {
		return fmt.Errorf("delete tenant from account service failed: %s", err)
	}
	return nil
}

func (s *svc) SyncTenants(ctx context.Context) error {
	go func() {
		ctx := s.WrapContext(context.Background())
		err := s.tenantSync.SyncTenants(ctx)
		if err != nil {
			log.FromContext(ctx).Error(err, "sync tenants manually failed")
		}
	}()
	return nil
}

func (s *svc) AddImpersonate(ctx context.Context, userEmail string, tenantID uint64) error {
	impersonateTenantID, isImpersonated, err := s.accountCli.GetImpersonateUser(ctx, userEmail)
	if err != nil {
		return err
	}
	if isImpersonated {
		if impersonateTenantID == tenantID {
			return nil
		}
		return fmt.Errorf("user %s is impersonating tenant %d", userEmail, impersonateTenantID)
	}
	if err := s.accountCli.AddImpersonateUser(ctx, tenantID, userEmail); err != nil {
		return err
	}
	return nil
}

func (s *svc) GetImpersonate(ctx context.Context, userEmail string) (*uint64, error) {
	impersonateTenantID, isImpersonated, err := s.accountCli.GetImpersonateUser(ctx, userEmail)
	if err != nil {
		return nil, err
	}
	if !isImpersonated {
		return nil, nil
	}
	return &impersonateTenantID, nil
}

func (s *svc) DeleteImpersonate(ctx context.Context, userEmail string) error {
	impersonateTenantID, isImpersonated, err := s.accountCli.GetImpersonateUser(ctx, userEmail)
	if err != nil {
		return err
	}
	if isImpersonated {
		if err := s.accountCli.DeleteImpersonateUser(ctx, impersonateTenantID, userEmail); err != nil {
			return err
		}
	}
	return nil
}

func (s *svc) DeleteProjectByID(ctx context.Context, tenantID, projectID, applicant string) error {
	if err := s.accountCli.DeleteProjectByID(ctx, tenantID, projectID, applicant); err != nil {
		return fmt.Errorf("delete project from account service failed: %s", err)
	}
	return nil
}
func (s *svc) AddUserToTenant(ctx context.Context, tenantID, userID, orgRole, applicant string) error {
	if err := s.accountCli.AddUserToTenant(ctx, tenantID, userID, orgRole, applicant); err != nil {
		return fmt.Errorf("add user to tenant from account service failed: %s", err)
	}
	return nil
}

func (s *svc) AddUserToProject(ctx context.Context, tenantID, projectID, userID, projectRole, applicant string) error {
	if err := s.accountCli.AddUserToProject(ctx, tenantID, projectID, userID, projectRole, applicant); err != nil {
		return fmt.Errorf("add user to project from account service failed: %s", err)
	}
	return nil
}

func (s *svc) RemoveUserFromTenant(ctx context.Context, tenantID, userID, applicant string) error {
	if err := s.accountCli.RemoveUserFromTenant(ctx, tenantID, userID, applicant); err != nil {
		return fmt.Errorf("remove user in tenant from account service failed: %s", err)
	}
	return nil
}

func (s *svc) RemoveUserFromProject(ctx context.Context, tenantID, projectID, userID, applicant string) error {
	if err := s.accountCli.RemoveUserFromProject(ctx, tenantID, projectID, userID, applicant); err != nil {
		return fmt.Errorf("remove user in project from account service failed: %s", err)
	}
	return nil
}

func (s *svc) DeleteUser(ctx context.Context, userID, applicant string) error {
	if err := s.accountCli.DeleteUser(ctx, userID, applicant); err != nil {
		return fmt.Errorf("delete user from account service failed: %s", err)
	}
	return nil
}

func (s *svc) GetTenantUsers(ctx context.Context, tenantID string, query accountcli.GetUsersReq, applicant string) (*accountcli.GetUsersResp, error) {
	users, err := s.accountCli.GetTenantUsers(ctx, tenantID, query, applicant)
	if err != nil {
		return nil, fmt.Errorf("get org users from account service failed: %s", err)
	}
	if query.FullEmail {
		_, err = s.buildAndRecordEvent(ctx, tenantID, event.TenantEventTypeViewEmail, applicant)
		if err != nil {
			return nil, err
		}
	}
	return users, nil
}

func (s *svc) GetProjectUsers(ctx context.Context, tenantID, projectID string, query accountcli.GetUsersReq, applicant string) (*accountcli.GetUsersResp, error) {
	users, err := s.accountCli.GetProjectUsers(ctx, tenantID, projectID, query, applicant)
	if err != nil {
		return nil, fmt.Errorf("get project users from account service failed: %s", err)
	}
	if query.FullEmail {
		_, err = s.buildAndRecordEvent(ctx, tenantID, event.TenantEventTypeViewEmail, applicant)
		if err != nil {
			return nil, err
		}
	}
	return users, nil
}

func (s *svc) GetProject(ctx context.Context, projectID uint64) (*accountcli.Project, error) {
	project, err := s.accountCli.GetProject(ctx, projectID)
	if err != nil {
		return nil, fmt.Errorf("get project from account service failed: %s", err)
	}
	result := &accountcli.Project{
		ID:             project.Id,
		Name:           project.Name,
		TenantID:       project.TenantId,
		Status:         project.Status,
		CreatedAt:      project.CreatedAt,
		UpdatedAt:      project.UpdatedAt,
		DeletedAt:      project.DeletedAt,
		IsCrossAccount: project.IsCrossAccount,
		AwsCmekEnabled: project.AwsCmekEnabled,
	}
	return result, nil
}

func (s *svc) buildAndRecordEvent(ctx context.Context, tenantID string, typ event.EventType, applicant string) (*event.BaseEvent, error) {
	id, err := strconv.ParseUint(tenantID, 10, 64)
	if err != nil {
		return nil, err
	}

	ev := event.BaseEvent{
		ID:            id,
		OperatorEmail: applicant,
		OperateAt:     pointer.Pointer(time.Now()),
		EventType:     typ,
	}

	auditLog, err := adsvc.EventToAuditLog(ev, auditent.AuditLogTypeTenant, nil)
	if err != nil {
		return &ev, err
	}

	err = s.auditSvc.Create(ctx, auditLog)
	if err != nil {
		return &ev, fmt.Errorf("create audit log failed: %w", err)
	}

	return &ev, nil
}

func (s *svc) MetricsAdvancedQueryRange(ctx context.Context,
	query string, start, end, step int64) (*tntent.MetricsQueryData, error) {
	timestamps := getTimestampByStep(start, end, step)
	result := make(chan map[string]datapoint, len(timestamps))
	var wg sync.WaitGroup
	for _, timestamp := range timestamps {
		wg.Add(1)
		go func(timestamp int64) {
			defer wg.Done()
			dataPoint, err := s.queryMetrics(ctx, query, timestamp)
			if err != nil {
				log.FromContext(ctx).Error(err, "query metrics failed")
				return
			}
			result <- dataPoint
		}(timestamp)
	}
	wg.Wait()
	close(result)

	clusterToDataPoints := make(map[string]datapoints)
	for dataPoint := range result {
		for clusterId, point := range dataPoint {
			if _, ok := clusterToDataPoints[clusterId]; !ok {
				clusterToDataPoints[clusterId] = make([]datapoint, 0)
			}
			clusterToDataPoints[clusterId] = append(clusterToDataPoints[clusterId], point)
		}
	}
	resp := tntent.MetricsQueryData{
		ResultType: "matrix",
	}
	for clusterId := range clusterToDataPoints {
		sort.Sort(clusterToDataPoints[clusterId])
		values := make([][]interface{}, 0, len(clusterToDataPoints[clusterId]))
		for _, point := range clusterToDataPoints[clusterId] {
			values = append(values, []interface{}{point.timestamp, point.value})
		}
		resp.Result = append(resp.Result, tntent.MetricsQueryDataResult{
			Metric: map[string]string{"cluster_id": clusterId},
			Values: values,
		})
	}
	log.FromContext(ctx).Info("query metrics success", "query", query,
		"start", start, "end", end, "step", step,
		"result", resp)

	return &resp, nil
}

type datapoint struct {
	timestamp int64
	value     float64
}

type datapoints []datapoint

var _ sort.Interface = datapoints{}

func (d datapoints) Len() int {
	return len(d)
}

func (d datapoints) Less(i, j int) bool {
	return d[i].timestamp < d[j].timestamp
}

func (d datapoints) Swap(i, j int) {
	d[i], d[j] = d[j], d[i]
}

func (s *svc) queryMetrics(ctx context.Context,
	query string, timestamp int64) (map[string]datapoint, error) {
	client, err := promapi.NewClient(promapi.Config{
		Address: s.conf.O11yEndpoint + s.conf.PrometheusPath,
	})
	if err != nil {
		return nil, fmt.Errorf("error creating client: %v", err)
	}
	v1api := promv1.NewAPI(client)
	result, warnings, err := v1api.Query(ctx,
		query,
		time.Unix(timestamp, 0),
		promv1.WithTimeout(5*time.Second),
	)
	if err != nil {
		return nil, fmt.Errorf("error querying Prometheus: %v", err)
	}
	if len(warnings) > 0 {
		log.FromContext(ctx).Info("query metrics query warnings", "warnings", warnings)
	}
	vector, ok := result.(prommodel.Vector)
	if !ok {
		return nil, fmt.Errorf("prometheus query result is not vector")
	}
	clusterToDataPoint := make(map[string]datapoint)
	for _, sample := range vector {
		clusterIdValue, ok := sample.Metric["cluster_id"]
		if !ok {
			return nil, fmt.Errorf("cluster_id not found in metrics")
		}
		clusterId := string(clusterIdValue)
		clusterToDataPoint[clusterId] = datapoint{
			timestamp: int64(int64(sample.Timestamp) / 1000),
			value:     float64(sample.Value),
		}
	}
	return clusterToDataPoint, nil
}

func getTimestampByStep(start, end, stepSecond int64) []int64 {
	var timestamps []int64
	for t := start; t <= end; t += stepSecond {
		timestamps = append(timestamps, t)
	}
	return timestamps
}
