package autocredit

import (
	"context"
	"fmt"
	"strings"
	"sync"
	"time"

	"github.com/go-logr/logr"
	"go.uber.org/dig"

	"github.com/tidbcloud/devops/entity/tenant"
	"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/service"
	tenantsvc "github.com/tidbcloud/devops/pkg/service/tenant"
)

const (
	billingConfigStatusActive = "Active"
	timeLayoutUTC             = "2006-01-02T15:04:05Z"
	defaultSheetID            = "1XWsXQ9tRdm7jHur945XyS3sKC1lDbky83RF7I9gIlGs"
	defaultSheetRange         = "Sheet1!A:A"
)

type ServiceParam struct {
	dig.In

	Conf       *config.RootConfig
	Logger     logr.Logger
	GoogleCli  googlecli.GoogleClient
	BillingCli billingcli.Client
	TenantSvc  tenantsvc.TenantServiceAPI
}

type ServiceResult struct {
	dig.Out

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

type svc struct {
	*service.JobService

	conf       config.AutoCreditServiceConfig
	googleCli  googlecli.GoogleClient
	billingCli billingcli.Client
	tenantSvc  tenantsvc.TenantServiceAPI
	mutex      *sync.Mutex
}

type runStat struct {
	totalRows       int
	validEmails     int
	matchedTenants  int
	alreadyCredited int
	missingTenant   int
	multiTenant     int
	noBillingConfig int
	created         int
	failed          int
}

func NewService(p ServiceParam) (ServiceResult, error) {
	s := &svc{
		JobService: service.NewJobService("AutoCreditService"),
		conf:       p.Conf.Services.AutoCredit,
		googleCli:  p.GoogleCli,
		billingCli: p.BillingCli,
		tenantSvc:  p.TenantSvc,
		mutex:      &sync.Mutex{},
	}

	if !s.conf.CronDisable {
		s.AddJob(service.MustNewCronJob("AutoCreditFromSheet", s.conf.CronSpec, s.RunOnce))
		p.Logger.WithName("AutoCreditService").Info("auto credit cron enabled",
			"cron_spec", s.conf.CronSpec,
			"dry_run", s.conf.DryRun,
			"sheet_id", firstNonEmpty(s.conf.SheetID, defaultSheetID),
			"sheet_range", firstNonEmpty(s.conf.SheetRange, defaultSheetRange),
			"operator_email", s.conf.OperatorEmail,
		)
	} else {
		p.Logger.WithName("AutoCreditService").Info("auto credit cron disabled")
	}

	return ServiceResult{Service: s}, nil
}

func (s *svc) RunOnce(ctx context.Context) error {
	if !s.mutex.TryLock() {
		s.Logger(ctx).Info("auto credit job is already running")
		return nil
	}
	defer s.mutex.Unlock()

	if s.googleCli == nil || s.googleCli.Sheets() == nil {
		return fmt.Errorf("google sheets client is not available")
	}

	sheetID := s.conf.SheetID
	if sheetID == "" {
		sheetID = defaultSheetID
	}
	sheetRange := s.conf.SheetRange
	if sheetRange == "" {
		sheetRange = defaultSheetRange
	}

	s.Logger(ctx).Info("auto credit run started",
		"sheet_id", sheetID,
		"sheet_range", sheetRange,
		"dry_run", s.conf.DryRun,
		"operator_email", s.conf.OperatorEmail,
	)

	rows, err := s.googleCli.Sheets().ReadRange(ctx, sheetID, sheetRange)
	if err != nil {
		s.Logger(ctx).Error(err, "failed to read auto credit sheet", "sheet_id", sheetID, "sheet_range", sheetRange)
		return err
	}

	stat := &runStat{totalRows: len(rows)}
	emails := normalizeEmails(rows)
	stat.validEmails = len(emails)

	for _, email := range emails {
		if err := s.processEmail(ctx, email, stat); err != nil {
			stat.failed++
			s.Logger(ctx).Error(err, "failed to process auto credit email", "email", email)
		}
	}

	s.Logger(ctx).Info("auto credit run finished",
		"rows", stat.totalRows,
		"valid_emails", stat.validEmails,
		"matched_tenants", stat.matchedTenants,
		"already_credited", stat.alreadyCredited,
		"missing_tenant", stat.missingTenant,
		"multiple_tenants", stat.multiTenant,
		"no_billing_config", stat.noBillingConfig,
		"created", stat.created,
		"failed", stat.failed,
		"dry_run", s.conf.DryRun,
	)
	return nil
}

func (s *svc) processEmail(ctx context.Context, email string, stat *runStat) error {
	targetTenant, err := s.findTenantByEmail(ctx, email)
	if err != nil {
		return err
	}
	if targetTenant == nil {
		stat.missingTenant++
		s.Logger(ctx).Info("skip auto credit because tenant not found", "email", email)
		return nil
	}
	if targetTenant.Id == 0 {
		stat.multiTenant++
		s.Logger(ctx).Info("skip auto credit because multiple exact-match tenants found", "email", email)
		return nil
	}
	stat.matchedTenants++

	billingConfig, err := s.findActiveBillingConfig(ctx, targetTenant.Id)
	if err != nil {
		return err
	}
	if billingConfig == nil {
		stat.noBillingConfig++
		s.Logger(ctx).Info("skip auto credit because active billing config not found", "tenant_id", targetTenant.Id, "email", email)
		return nil
	}

	note := s.noteFor(email)
	credited, err := s.hasCreditWithNote(ctx, targetTenant.Id, billingConfig.BillingConfigID, note)
	if err != nil {
		return err
	}
	if credited {
		stat.alreadyCredited++
		s.Logger(ctx).Info("skip auto credit because note already exists", "tenant_id", targetTenant.Id, "email", email, "note", note)
		return nil
	}

	req := s.buildCreateCreditsReq(note)
	if s.conf.DryRun {
		s.Logger(ctx).Info("dry run auto credit create",
			"tenant_id", targetTenant.Id,
			"billing_config_id", billingConfig.BillingConfigID,
			"email", email,
			"note", note,
			"amount", req.TotalCredits,
		)
		return nil
	}

	if err := s.billingCli.CreateCreditsWithBillingConfig(ctx, targetTenant.Id, billingConfig.BillingConfigID, req, s.conf.OperatorEmail); err != nil {
		return err
	}
	stat.created++
	s.Logger(ctx).Info("auto credit created",
		"tenant_id", targetTenant.Id,
		"billing_config_id", billingConfig.BillingConfigID,
		"email", email,
		"note", note,
		"amount", req.TotalCredits,
	)
	return nil
}

func (s *svc) findTenantByEmail(ctx context.Context, email string) (*tenant.Tenant, error) {
	tenants, _, err := s.tenantSvc.ListTenants(ctx, tenantsvc.ListTenantsParam{
		Filter: tenantsvc.ListTenantsFilter{
			Status: []tenant.TenantStatus{tenant.TenantStatusActive},
		},
		Search:  email,
		Page:    1,
		PerPage: 100,
	})
	if err != nil {
		return nil, err
	}

	var exactMatches []*tenant.Tenant
	for _, current := range tenants {
		if strings.EqualFold(strings.TrimSpace(current.Email), email) {
			exactMatches = append(exactMatches, current)
		}
	}

	switch len(exactMatches) {
	case 0:
		return nil, nil
	case 1:
		return exactMatches[0], nil
	default:
		// Use zero ID as a sentinel so caller can distinguish "not found" vs "multiple".
		return &tenant.Tenant{}, nil
	}
}

func (s *svc) findActiveBillingConfig(ctx context.Context, tenantID uint64) (*billingcli.BillingConfig, error) {
	billingConfigs, err := s.billingCli.ListBillingConfigs(ctx, tenantID, s.conf.OperatorEmail)
	if err != nil {
		return nil, err
	}

	var activeConfigs []*billingcli.BillingConfig
	for _, billingConfig := range billingConfigs {
		if billingConfig != nil && billingConfig.Status == billingConfigStatusActive {
			activeConfigs = append(activeConfigs, billingConfig)
		}
	}

	if len(activeConfigs) != 1 {
		return nil, nil
	}
	return activeConfigs[0], nil
}

func (s *svc) hasCreditWithNote(ctx context.Context, tenantID uint64, billingConfigID uint64, note string) (bool, error) {
	credits, err := s.billingCli.ListCredits(ctx, tenantID, billingConfigID, s.conf.OperatorEmail)
	if err != nil {
		return false, err
	}
	for _, credit := range credits {
		if credit != nil && strings.TrimSpace(credit.Note) == note {
			return true, nil
		}
	}
	return false, nil
}

func (s *svc) buildCreateCreditsReq(note string) *billingcli.CreateCreditsWithBillingConfigReq {
	startTime := time.Now().UTC().Truncate(time.Second)
	expiredAfter := 90 * 24 * time.Hour
	if s.conf.ExpiredAfter != nil {
		expiredAfter = *s.conf.ExpiredAfter
	}

	req := &billingcli.CreateCreditsWithBillingConfigReq{
		DiscountEnabled: s.conf.DiscountEnabled,
		ExpiredDate:     startTime.Add(expiredAfter).Format(timeLayoutUTC),
		Note:            note,
		Provider:        s.conf.Provider,
		StartDate:       startTime.Format(timeLayoutUTC),
		TotalCredits:    s.conf.CreditAmount,
		Usage:           s.conf.Usage,
	}
	if len(s.conf.BindTypeIDs) > 0 {
		req.BindTypes = make([]*billingcli.UnitFilterSlim, 0, len(s.conf.BindTypeIDs))
		for _, bindTypeID := range s.conf.BindTypeIDs {
			req.BindTypes = append(req.BindTypes, &billingcli.UnitFilterSlim{TypeID: bindTypeID})
		}
	}
	return req
}

func (s *svc) noteFor(email string) string {
	return fmt.Sprintf("%s:%s", s.conf.NotePrefix, email)
}

func normalizeEmails(rows [][]any) []string {
	seen := make(map[string]struct{}, len(rows))
	emails := make([]string, 0, len(rows))
	for _, row := range rows {
		if len(row) == 0 {
			continue
		}
		email := strings.ToLower(strings.TrimSpace(fmt.Sprint(row[0])))
		if email == "" || email == "<nil>" || email == "email" {
			continue
		}
		if _, ok := seen[email]; ok {
			continue
		}
		seen[email] = struct{}{}
		emails = append(emails, email)
	}
	return emails
}

func firstNonEmpty(values ...string) string {
	for _, value := range values {
		if value != "" {
			return value
		}
	}
	return ""
}
