package activeemeatenant

import (
	"bytes"
	"context"
	"encoding/csv"
	"encoding/json"
	"fmt"
	"net/mail"
	"net/url"
	"sort"
	"strconv"
	"strings"
	"sync"
	"time"

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

	tntent "github.com/tidbcloud/devops/entity/tenant"
	activeemeamod "github.com/tidbcloud/devops/models/activeemea"
	"github.com/tidbcloud/devops/pkg/infra/asset"
	"github.com/tidbcloud/devops/pkg/infra/clients/accountcli"
	"github.com/tidbcloud/devops/pkg/infra/clients/billingcli"
	"github.com/tidbcloud/devops/pkg/infra/clients/coresvccli"
	"github.com/tidbcloud/devops/pkg/infra/clients/larkcli"
	"github.com/tidbcloud/devops/pkg/infra/clients/llm_cli"
	"github.com/tidbcloud/devops/pkg/infra/config"
	"github.com/tidbcloud/devops/pkg/service"
	tenantsvc "github.com/tidbcloud/devops/pkg/service/tenant"
	"github.com/tidbcloud/devops/pkg/service/tenant/region"
	"github.com/tidbcloud/pkgv2/db"
	mgmtv1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_mgmt_service/v1"
)

const (
	serviceName                = "ActiveEMEATenantReport"
	defaultPageSize            = 500
	defaultMinClusterCount     = 1
	defaultCandidateProfileTTL = 24 * time.Hour
	defaultEnrichmentTTL       = 30 * 24 * time.Hour
	defaultEnrichmentVersion   = "v1"
	billingConfigStatusActive  = "Active"
	larkReceiveTypeChat        = "chat_id"
)

type ServiceParam struct {
	dig.In

	Conf       *config.RootConfig
	Logger     logr.Logger
	DBProvider db.TransProvider
	Asset      asset.Asset
	TenantSvc  tenantsvc.TenantServiceAPI
	AccountCli accountcli.Client
	CoreSvcCli *coresvccli.CoreSvcCli
	BillingCli billingcli.Client
	LarkCli    *larkcli.LarkClient
	LLMCli     *llm_cli.LLMCli
}

type ServiceResult struct {
	dig.Out

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

type svc struct {
	*service.JobService

	conf       config.ActiveEMEATenantReportConfig
	logger     logr.Logger
	repo       Repository
	asset      asset.Asset
	tenantSvc  tenantsvc.TenantServiceAPI
	accountCli accountcli.Client
	coreSvcCli *coresvccli.CoreSvcCli
	billingCli billingcli.Client
	larkCli    *larkcli.LarkClient
	llmCli     *llm_cli.LLMCli
	mutex      *sync.Mutex
}

type runStat struct {
	candidateTenants int
	includedTenants  int
	skippedCountry   int
	skippedCluster   int
	skippedCard      int
	skippedDomain    int
	skippedHQ        int
	failed           int
}

type validCard struct {
	BillingConfigID uint64
	PaymentMethod   *billingcli.StripePaymentMethod
}

type reportItem struct {
	ReportDate             string
	TenantID               uint64
	TenantName             string
	OrgEmail               string
	EmailDomain            string
	AccountCountry         string
	TenantRegion           string
	ClusterCount           int
	ClusterIDs             string
	BillingConfigID        uint64
	CardBrand              string
	CardCountry            string
	CardLast4              string
	CompanyName            string
	Headquarter            string
	HeadquarterCountryCode string
	HeadquarterRegion      string
	ConfidenceLevel        string
	OpsURL                 string
}

type domainEnrichmentPayload struct {
	CompanyName            string `json:"company_name"`
	Headquarter            string `json:"headquarter"`
	HeadquarterCountryCode string `json:"headquarter_country_code"`
	Vertical               string `json:"vertical"`
	BusinessModel          string `json:"business_model"`
	AIConfidenceLevel      string `json:"ai_confidence_level"`
}

type reportCardMessage struct {
	Config   reportCardConfig    `json:"config"`
	Header   reportCardHeader    `json:"header"`
	Elements []reportCardElement `json:"elements"`
}

type reportCardConfig struct {
	WideScreenMode bool `json:"wide_screen_mode"`
}

type reportCardHeader struct {
	Template string         `json:"template"`
	Title    reportCardText `json:"title"`
}

type reportCardElement struct {
	Tag  string         `json:"tag"`
	Text reportCardText `json:"text"`
}

type reportCardText struct {
	Tag     string `json:"tag"`
	Content string `json:"content"`
}

func (m reportCardMessage) Type() string {
	return "interactive"
}

func NewService(p ServiceParam) (ServiceResult, error) {
	conf := p.Conf.Services.ActiveEMEATenant
	larkCli := p.LarkCli
	if strings.TrimSpace(conf.LarkAppCode) != "" || strings.TrimSpace(conf.LarkAppSecret) != "" {
		if strings.TrimSpace(conf.LarkAppCode) == "" || strings.TrimSpace(conf.LarkAppSecret) == "" {
			return ServiceResult{}, fmt.Errorf("active EMEA tenant report dedicated lark app requires both larkAppCode and larkAppSecret")
		}
		larkCli = larkcli.NewWithApp(conf.LarkAppCode, conf.LarkAppSecret, p.Conf.Clients.Lark.Global.LarkBaseURL)
	}

	s := &svc{
		JobService: service.NewJobService(serviceName),
		conf:       conf,
		logger:     p.Logger.WithName(serviceName),
		repo:       NewRepository(p.DBProvider),
		asset:      p.Asset,
		tenantSvc:  p.TenantSvc,
		accountCli: p.AccountCli,
		coreSvcCli: p.CoreSvcCli,
		billingCli: p.BillingCli,
		larkCli:    larkCli,
		llmCli:     p.LLMCli,
		mutex:      &sync.Mutex{},
	}

	s.logger.Info("active EMEA tenant report effective config loaded",
		"cron_disable", s.conf.CronDisable,
		"cron_spec", s.conf.CronSpec,
		"dry_run", s.conf.DryRun,
		"operator_email", s.conf.OperatorEmail,
		"page_size", s.pageSize(),
		"account_country_allow_list", s.effectiveAccountCountryAllowList(),
		"min_active_cluster_count", s.minActiveClusterCount(),
		"candidate_profile_ttl", s.candidateProfileTTL().String(),
		"enrichment_ttl", s.enrichmentTTL().String(),
		"enrichment_schema_version", s.enrichmentSchemaVersion(),
		"report_s3_prefix", s.reportS3Prefix(),
		"notify_chat_id", s.conf.NotifyChatID,
		"lark_folder_token", s.conf.LarkFolderToken,
		"dedicated_lark_app", strings.TrimSpace(s.conf.LarkAppCode) != "",
		"ops_portal_base_url", s.conf.OpsPortalBaseURL,
	)

	if !s.conf.CronDisable {
		s.AddJob(service.MustNewCronJob(serviceName, s.conf.CronSpec, s.RunOnce))
		s.logger.Info("active EMEA tenant report cron enabled",
			"cron_spec", s.conf.CronSpec,
			"dry_run", s.conf.DryRun,
			"notify_chat_id", s.conf.NotifyChatID,
			"dedicated_lark_app", strings.TrimSpace(s.conf.LarkAppCode) != "",
		)
	} else {
		s.logger.Info("active EMEA tenant report cron disabled")
	}

	return ServiceResult{Service: s}, nil
}

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

	now := time.Now().UTC()
	reportDate := now.Format(time.DateOnly)
	stat := &runStat{}

	s.Logger(ctx).Info("active EMEA tenant report run started",
		"report_date", reportDate,
		"dry_run", s.conf.DryRun,
		"account_country_allow_list", s.accountCountryAllowList(),
	)

	candidates, err := s.listCandidateTenants(ctx)
	if err != nil {
		return err
	}
	stat.candidateTenants = len(candidates)
	if !s.conf.DryRun {
		if err := s.repo.UpsertTenantCandidateSnapshots(ctx, baseTenantCandidateSnapshots(candidates, now)); err != nil {
			return err
		}
	}

	items := make([]*reportItem, 0, len(candidates))
	for _, tenant := range candidates {
		item, include, err := s.buildReportItem(ctx, tenant, reportDate, now, stat)
		if err != nil {
			stat.failed++
			s.Logger(ctx).Error(err, "failed to process active EMEA tenant candidate", "tenant_id", tenant.Id)
			continue
		}
		if include {
			items = append(items, item)
			stat.includedTenants++
		}
	}

	sort.Slice(items, func(i, j int) bool {
		return items[i].TenantID < items[j].TenantID
	})

	if s.conf.DryRun {
		s.logFinished(ctx, stat, "", "")
		return nil
	}

	reportKey, err := s.writeReportCSVToS3(ctx, reportDate, now, items)
	if err != nil {
		return err
	}

	spreadsheetURL, err := s.publishToLark(ctx, reportDate, items, stat, reportKey)
	if err != nil {
		return err
	}
	s.logFinished(ctx, stat, spreadsheetURL, reportKey)
	return nil
}

func (s *svc) listCandidateTenants(ctx context.Context) ([]*tntent.Tenant, error) {
	pageSize := s.pageSize()
	page := 1
	var tenants []*tntent.Tenant
	for {
		current, total, err := s.tenantSvc.ListTenants(ctx, tenantsvc.ListTenantsParam{
			Filter: tenantsvc.ListTenantsFilter{
				Status: []tntent.TenantStatus{tntent.TenantStatusActive},
				Kind:   []tntent.TenantKind{tntent.TenantKindExternal},
				Region: []tntent.TenantRegion{tntent.TenantRegionEMEA},
			},
			Page:    page,
			PerPage: pageSize,
		})
		if err != nil {
			return nil, fmt.Errorf("list active external EMEA tenants failed: %w", err)
		}
		tenants = append(tenants, current...)
		if len(tenants) >= int(total) || len(current) == 0 {
			break
		}
		page++
	}
	return tenants, nil
}

func (s *svc) buildReportItem(ctx context.Context, tenant *tntent.Tenant, reportDate string, now time.Time, stat *runStat) (*reportItem, bool, error) {
	if tenant == nil {
		return nil, false, nil
	}

	candidate, err := s.loadCandidateProfile(ctx, tenant, now)
	if err != nil {
		return nil, false, err
	}

	persistCandidate := func(reason string) {
		candidate.LastSkipReason = reason
		if s.conf.DryRun {
			return
		}
		if err := s.repo.UpsertTenantCandidates(ctx, []*activeemeamod.TenantCandidate{candidate}); err != nil {
			s.Logger(ctx).Error(err, "failed to upsert active EMEA tenant candidate", "tenant_id", tenant.Id)
		}
	}

	if !s.accountCountryAllowed(candidate.AccountCountry) {
		stat.skippedCountry++
		persistCandidate("account_country_not_allowed")
		return nil, false, nil
	}

	clusters, clusterCount, err := s.coreSvcCli.Mgmt.ListClusters(ctx, coresvccli.ListClustersParam{
		TenantIDs: []uint64{tenant.Id},
		Page:      1,
		PerPage:   1000,
	})
	if err != nil {
		return nil, false, fmt.Errorf("list clusters for tenant %d failed: %w", tenant.Id, err)
	}
	candidate.LastClusterCount = int(clusterCount)
	candidate.LastClusterIDs = joinClusterIDs(clusters)
	candidate.LastClusterSyncedAt = ptrTime(now)
	if int(clusterCount) < s.minActiveClusterCount() {
		stat.skippedCluster++
		persistCandidate("no_undeleted_cluster")
		return nil, false, nil
	}

	card, err := s.findValidCreditCard(ctx, tenant.Id, candidate.BillingConfigID, now)
	if err != nil {
		return nil, false, fmt.Errorf("find valid credit card for tenant %d failed: %w", tenant.Id, err)
	}
	if card == nil {
		stat.skippedCard++
		candidate.BillingConfigStatus = ""
		candidate.LastBillingSyncedAt = ptrTime(now)
		persistCandidate("no_non_expired_card")
		return nil, false, nil
	}
	candidate.BillingConfigID = card.BillingConfigID
	candidate.BillingConfigStatus = billingConfigStatusActive
	candidate.LastBillingSyncedAt = ptrTime(now)

	if candidate.EmailDomain == "" {
		stat.skippedDomain++
		persistCandidate("missing_email_domain")
		return nil, false, nil
	}
	enrichment, err := s.getDomainEnrichment(ctx, candidate.EmailDomain, now)
	if err != nil {
		return nil, false, fmt.Errorf("get domain enrichment %s failed: %w", candidate.EmailDomain, err)
	}
	if enrichment == nil || enrichment.HeadquarterRegion != string(tntent.TenantRegionEMEA) {
		stat.skippedHQ++
		persistCandidate("headquarter_not_emea")
		return nil, false, nil
	}
	candidate.LastIncludedAt = ptrTime(now)
	persistCandidate("")

	return &reportItem{
		ReportDate:             reportDate,
		TenantID:               tenant.Id,
		TenantName:             firstNonEmpty(candidate.AccountName, candidate.TenantName),
		OrgEmail:               firstNonEmpty(candidate.AccountEmail, candidate.TenantEmail),
		EmailDomain:            candidate.EmailDomain,
		AccountCountry:         candidate.AccountCountry,
		TenantRegion:           string(tenant.GetTenantRegion()),
		ClusterCount:           int(clusterCount),
		ClusterIDs:             candidate.LastClusterIDs,
		BillingConfigID:        card.BillingConfigID,
		CardBrand:              strings.TrimSpace(card.PaymentMethod.Card.Brand),
		CardCountry:            strings.ToUpper(strings.TrimSpace(card.PaymentMethod.Card.Country)),
		CardLast4:              strings.TrimSpace(card.PaymentMethod.Card.Last4),
		CompanyName:            enrichment.CompanyName,
		Headquarter:            enrichment.Headquarter,
		HeadquarterCountryCode: enrichment.HeadquarterCountryCode,
		HeadquarterRegion:      enrichment.HeadquarterRegion,
		ConfidenceLevel:        enrichment.ConfidenceLevel,
		OpsURL:                 s.tenantOpsURL(tenant.Id),
	}, true, nil
}

func (s *svc) loadCandidateProfile(ctx context.Context, tenant *tntent.Tenant, now time.Time) (*activeemeamod.TenantCandidate, error) {
	cached, err := s.repo.GetTenantCandidate(ctx, tenant.Id)
	if err != nil {
		return nil, err
	}
	candidate := baseTenantCandidateSnapshot(tenant, now)
	if cached != nil {
		candidate = cached
		candidate.TenantName = tenant.Name
		candidate.TenantEmail = tenant.Email
		candidate.TenantStatus = string(tenant.Status)
		candidate.TenantKind = string(tenant.Kind)
		candidate.TenantRegion = string(tenant.GetTenantRegion())
		candidate.LastCandidateAt = ptrTime(now)
	}
	if !shouldRefreshProfile(candidate.LastAccountSyncedAt, s.candidateProfileTTL(), now) &&
		candidate.AccountCountry != "" && candidate.AccountEmail != "" {
		return candidate, nil
	}

	org, err := s.accountCli.GetTenant(ctx, tenant.Id)
	if err != nil {
		return nil, fmt.Errorf("get account tenant %d failed: %w", tenant.Id, err)
	}
	candidate.AccountName = strings.TrimSpace(org.GetName())
	candidate.AccountEmail = strings.TrimSpace(org.GetEmail())
	candidate.AccountCountry = strings.ToUpper(strings.TrimSpace(org.GetCountry()))
	candidate.EmailDomain = emailDomain(firstNonEmpty(candidate.AccountEmail, candidate.TenantEmail))
	candidate.LastAccountSyncedAt = ptrTime(now)
	return candidate, nil
}

func (s *svc) findValidCreditCard(ctx context.Context, tenantID uint64, cachedBillingConfigID uint64, now time.Time) (*validCard, error) {
	if cachedBillingConfigID != 0 {
		card, err := s.findValidCreditCardInBillingConfig(ctx, tenantID, cachedBillingConfigID, now)
		if err == nil && card != nil {
			return card, nil
		}
	}

	billingConfigs, err := s.billingCli.ListBillingConfigs(ctx, tenantID, s.conf.OperatorEmail)
	if err != nil {
		return nil, err
	}
	for _, billingConfig := range billingConfigs {
		if billingConfig == nil || !strings.EqualFold(strings.TrimSpace(billingConfig.Status), billingConfigStatusActive) {
			continue
		}
		card, err := s.findValidCreditCardInBillingConfig(ctx, tenantID, billingConfig.BillingConfigID, now)
		if err != nil {
			return nil, err
		}
		if card != nil {
			return card, nil
		}
	}
	return nil, nil
}

func (s *svc) findValidCreditCardInBillingConfig(ctx context.Context, tenantID uint64, billingConfigID uint64, now time.Time) (*validCard, error) {
	paymentMethods, err := s.billingCli.ListStripePaymentMethods(ctx, tenantID, billingConfigID, s.conf.OperatorEmail)
	if err != nil {
		return nil, err
	}
	for _, method := range paymentMethods {
		if method == nil || method.Card == nil || strings.TrimSpace(method.ID) == "" {
			continue
		}
		if strings.ToLower(strings.TrimSpace(method.Card.Funding)) != "credit" {
			continue
		}
		if billingcli.IsNonExpiredStripeCard(method.Card, now) {
			return &validCard{BillingConfigID: billingConfigID, PaymentMethod: method}, nil
		}
	}
	return nil, nil
}

func (s *svc) getDomainEnrichment(ctx context.Context, domain string, now time.Time) (*activeemeamod.CompanyDomainEnrichment, error) {
	cached, err := s.repo.GetDomainEnrichment(ctx, domain)
	if err != nil {
		return nil, err
	}
	if isDomainEnrichmentFresh(cached, s.enrichmentSchemaVersion(), now) {
		return cached, nil
	}
	if s.llmCli == nil {
		return nil, fmt.Errorf("LLM client is not available")
	}

	raw, err := s.llmCli.SimpleSearch(buildEnrichmentPrompt(domain))
	if err != nil {
		return nil, err
	}
	payload, err := parseDomainEnrichment(raw)
	if err != nil {
		return nil, err
	}

	countryCode := strings.ToUpper(strings.TrimSpace(payload.HeadquarterCountryCode))
	headquarterRegion := string(region.CountryToRegion(countryCode))
	enrichment := &activeemeamod.CompanyDomainEnrichment{
		Domain:                 domain,
		CompanyName:            firstNonEmpty(strings.TrimSpace(payload.CompanyName), "Unknown"),
		Headquarter:            firstNonEmpty(strings.TrimSpace(payload.Headquarter), "Unknown"),
		HeadquarterCountryCode: countryCode,
		HeadquarterRegion:      headquarterRegion,
		Vertical:               firstNonEmpty(strings.TrimSpace(payload.Vertical), "Unknown"),
		BusinessModel:          firstNonEmpty(strings.TrimSpace(payload.BusinessModel), "Unknown"),
		ConfidenceLevel:        firstNonEmpty(strings.TrimSpace(payload.AIConfidenceLevel), "low"),
		RawEnrichment:          raw,
		Source:                 "llm_simple_search",
		SchemaVersion:          s.enrichmentSchemaVersion(),
		ExpiresAt:              ptrTime(now.Add(s.enrichmentTTL())),
	}
	if s.conf.DryRun {
		return enrichment, nil
	}
	if err := s.repo.UpsertDomainEnrichment(ctx, enrichment); err != nil {
		return nil, err
	}
	return enrichment, nil
}

func (s *svc) writeReportCSVToS3(ctx context.Context, reportDate string, generatedAt time.Time, items []*reportItem) (string, error) {
	if s.asset == nil {
		return "", fmt.Errorf("asset client is not available")
	}
	bucket, err := s.asset.Bucket(ctx)
	if err != nil {
		return "", fmt.Errorf("open active EMEA report bucket failed: %w", err)
	}
	defer bucket.Close()

	key := s.reportS3Key(reportDate, generatedAt)
	data, err := reportCSV(items)
	if err != nil {
		return "", err
	}
	if err := bucket.WriteAll(ctx, key, data, nil); err != nil {
		return "", fmt.Errorf("write active EMEA report CSV %s failed: %w", key, err)
	}
	return key, nil
}

func (s *svc) publishToLark(ctx context.Context, reportDate string, items []*reportItem, stat *runStat, reportKey string) (string, error) {
	if s.larkCli == nil || s.larkCli.Sheets == nil {
		return "", fmt.Errorf("lark sheets client is not available")
	}
	spreadsheet, err := s.larkCli.Sheets.CreateSpreadsheet(ctx, fmt.Sprintf("Active EMEA Tenant Report %s", reportDate), s.conf.LarkFolderToken)
	if err != nil {
		return "", err
	}
	if err := s.larkCli.Sheets.WriteRange(ctx, spreadsheet.Token, spreadsheet.SheetID, "A1", reportRows(items)); err != nil {
		return "", err
	}
	if strings.TrimSpace(s.conf.NotifyChatID) != "" {
		if _, err := s.larkCli.IM.SendMessage(ctx, larkReceiveTypeChat, s.conf.NotifyChatID, buildReportCard(reportDate, stat.includedTenants, spreadsheet.URL)); err != nil {
			return "", err
		}
	}
	return spreadsheet.URL, nil
}

func buildReportCard(reportDate string, includedTenants int, sheetURL string) reportCardMessage {
	return reportCardMessage{
		Config: reportCardConfig{
			WideScreenMode: true,
		},
		Header: reportCardHeader{
			Template: "blue",
			Title: reportCardText{
				Tag:     "plain_text",
				Content: fmt.Sprintf("Active EMEA tenant report %s generated.", reportDate),
			},
		},
		Elements: []reportCardElement{
			{
				Tag: "div",
				Text: reportCardText{
					Tag: "lark_md",
					Content: fmt.Sprintf(
						"**Active Tenants:** %d\n**Sheet:** [Open Report](%s)",
						includedTenants,
						sheetURL,
					),
				},
			},
		},
	}
}

func (s *svc) logFinished(ctx context.Context, stat *runStat, spreadsheetURL string, reportKey string) {
	s.Logger(ctx).Info("active EMEA tenant report run finished",
		"candidate_tenants", stat.candidateTenants,
		"included_tenants", stat.includedTenants,
		"skipped_country", stat.skippedCountry,
		"skipped_cluster", stat.skippedCluster,
		"skipped_card", stat.skippedCard,
		"skipped_domain", stat.skippedDomain,
		"skipped_headquarter", stat.skippedHQ,
		"failed", stat.failed,
		"dry_run", s.conf.DryRun,
		"spreadsheet_url", spreadsheetURL,
		"report_s3_key", reportKey,
	)
}

func (s *svc) pageSize() int {
	if s.conf.PageSize > 0 {
		return s.conf.PageSize
	}
	return defaultPageSize
}

func (s *svc) minActiveClusterCount() int {
	if s.conf.MinActiveClusterCount != nil && *s.conf.MinActiveClusterCount >= 0 {
		return *s.conf.MinActiveClusterCount
	}
	return defaultMinClusterCount
}

func (s *svc) candidateProfileTTL() time.Duration {
	if s.conf.CandidateProfileTTL != nil && *s.conf.CandidateProfileTTL > 0 {
		return *s.conf.CandidateProfileTTL
	}
	return defaultCandidateProfileTTL
}

func (s *svc) enrichmentTTL() time.Duration {
	if s.conf.EnrichmentTTL != nil && *s.conf.EnrichmentTTL > 0 {
		return *s.conf.EnrichmentTTL
	}
	return defaultEnrichmentTTL
}

func (s *svc) enrichmentSchemaVersion() string {
	if strings.TrimSpace(s.conf.EnrichmentSchemaVersion) != "" {
		return strings.TrimSpace(s.conf.EnrichmentSchemaVersion)
	}
	return defaultEnrichmentVersion
}

func (s *svc) reportS3Prefix() string {
	prefix := strings.Trim(strings.TrimSpace(s.conf.ReportS3Prefix), "/")
	if prefix == "" {
		return "active-emea-tenant-report"
	}
	return prefix
}

func (s *svc) effectiveAccountCountryAllowList() []string {
	countries := s.conf.AccountCountryAllowList
	if len(countries) == 0 {
		countries = []string{"DE", "GB"}
	}
	seen := make(map[string]struct{}, len(countries))
	allowed := make([]string, 0, len(countries))
	for _, country := range countries {
		country = strings.ToUpper(strings.TrimSpace(country))
		if country == "" {
			continue
		}
		if _, ok := seen[country]; ok {
			continue
		}
		seen[country] = struct{}{}
		allowed = append(allowed, country)
	}
	sort.Strings(allowed)
	return allowed
}

func (s *svc) accountCountryAllowList() map[string]struct{} {
	countries := s.effectiveAccountCountryAllowList()
	allowed := make(map[string]struct{}, len(countries))
	for _, country := range countries {
		allowed[country] = struct{}{}
	}
	return allowed
}

func (s *svc) accountCountryAllowed(country string) bool {
	_, ok := s.accountCountryAllowList()[strings.ToUpper(strings.TrimSpace(country))]
	return ok
}

func (s *svc) tenantOpsURL(tenantID uint64) string {
	baseURL := strings.TrimRight(firstNonEmpty(strings.TrimSpace(s.conf.OpsPortalBaseURL), "https://ops.tidbcloud.com"), "/")
	return fmt.Sprintf("%s/orgs/%d", baseURL, tenantID)
}

func (s *svc) reportS3Key(reportDate string, generatedAt time.Time) string {
	return fmt.Sprintf("%s/date=%s/report_%s.csv", s.reportS3Prefix(), reportDate, generatedAt.Format("20060102150405"))
}

func reportCSV(items []*reportItem) ([]byte, error) {
	buf := bytes.NewBuffer(nil)
	writer := csv.NewWriter(buf)
	for _, row := range reportRows(items) {
		if err := writer.Write(row); err != nil {
			return nil, fmt.Errorf("write active EMEA report CSV row failed: %w", err)
		}
	}
	writer.Flush()
	if err := writer.Error(); err != nil {
		return nil, fmt.Errorf("flush active EMEA report CSV failed: %w", err)
	}
	return buf.Bytes(), nil
}

func reportRows(items []*reportItem) [][]string {
	rows := [][]string{{
		"Report Date", "Tenant ID", "Tenant Name", "Org Email", "Email Domain", "Account Country", "Tenant Region",
		"Cluster Count", "Cluster IDs", "Billing Config ID", "Card Brand", "Card Country", "Card Last4",
		"Company Name", "Headquarter", "Headquarter Country Code", "Headquarter Region", "Confidence Level", "Ops URL",
	}}
	for _, item := range items {
		rows = append(rows, []string{
			item.ReportDate,
			strconv.FormatUint(item.TenantID, 10),
			item.TenantName,
			item.OrgEmail,
			item.EmailDomain,
			item.AccountCountry,
			item.TenantRegion,
			strconv.Itoa(item.ClusterCount),
			item.ClusterIDs,
			strconv.FormatUint(item.BillingConfigID, 10),
			item.CardBrand,
			item.CardCountry,
			item.CardLast4,
			item.CompanyName,
			item.Headquarter,
			item.HeadquarterCountryCode,
			item.HeadquarterRegion,
			item.ConfidenceLevel,
			item.OpsURL,
		})
	}
	return rows
}

func baseTenantCandidateSnapshots(tenants []*tntent.Tenant, now time.Time) []*activeemeamod.TenantCandidate {
	candidates := make([]*activeemeamod.TenantCandidate, 0, len(tenants))
	for _, tenant := range tenants {
		if tenant == nil {
			continue
		}
		candidates = append(candidates, baseTenantCandidateSnapshot(tenant, now))
	}
	return candidates
}

func baseTenantCandidateSnapshot(tenant *tntent.Tenant, now time.Time) *activeemeamod.TenantCandidate {
	return &activeemeamod.TenantCandidate{
		TenantID:        tenant.Id,
		TenantName:      strings.TrimSpace(tenant.Name),
		TenantEmail:     strings.TrimSpace(tenant.Email),
		TenantStatus:    string(tenant.Status),
		TenantKind:      string(tenant.Kind),
		TenantRegion:    string(tenant.GetTenantRegion()),
		LastCandidateAt: ptrTime(now),
	}
}

func joinClusterIDs(clusters []*mgmtv1.Cluster) string {
	ids := make([]string, 0, len(clusters))
	for _, cluster := range clusters {
		if cluster != nil {
			ids = append(ids, strconv.FormatUint(cluster.ClusterId, 10))
		}
	}
	return strings.Join(ids, ",")
}

func isDomainEnrichmentFresh(enrichment *activeemeamod.CompanyDomainEnrichment, schemaVersion string, now time.Time) bool {
	if enrichment == nil {
		return false
	}
	if enrichment.SchemaVersion != schemaVersion {
		return false
	}
	if enrichment.ExpiresAt != nil && !enrichment.ExpiresAt.After(now) {
		return false
	}
	return enrichment.HeadquarterCountryCode != ""
}

func shouldRefreshProfile(lastSyncedAt *time.Time, ttl time.Duration, now time.Time) bool {
	if lastSyncedAt == nil {
		return true
	}
	return !lastSyncedAt.Add(ttl).After(now)
}

func parseDomainEnrichment(raw string) (*domainEnrichmentPayload, error) {
	cleaned := strings.TrimSpace(raw)
	cleaned = strings.TrimPrefix(cleaned, "```json")
	cleaned = strings.TrimPrefix(cleaned, "```")
	cleaned = strings.TrimSuffix(cleaned, "```")
	cleaned = strings.TrimSpace(cleaned)
	var payload domainEnrichmentPayload
	if err := json.Unmarshal([]byte(cleaned), &payload); err != nil {
		return nil, fmt.Errorf("parse domain enrichment JSON failed: %w", err)
	}
	return &payload, nil
}

func buildEnrichmentPrompt(domain string) string {
	return fmt.Sprintf(`Search the web to identify the company that uses the exact email domain "%s".

Verification requirements:
1. Prefer the company's official website and public company profiles.
2. Only infer headquarters if there is strong evidence.
3. Return ISO 3166-1 alpha-2 country code in "headquarter_country_code".
4. If the company cannot be verified, use "Unknown" for text fields and "" for the country code.

Respond ONLY with valid JSON:
{
  "company_name": "company name or Unknown",
  "headquarter": "city/country or Unknown",
  "headquarter_country_code": "ISO alpha-2 country code or empty string",
  "vertical": "industry vertical or Unknown",
  "business_model": "B2B/B2C/B2B2C or Unknown",
  "ai_confidence_level": "high/medium/low"
}`, domain)
}

func emailDomain(email string) string {
	email = strings.TrimSpace(strings.ToLower(email))
	if email == "" {
		return ""
	}
	if address, err := mail.ParseAddress(email); err == nil {
		email = address.Address
	}
	at := strings.LastIndex(email, "@")
	if at < 0 || at == len(email)-1 {
		return ""
	}
	domain := strings.Trim(email[at+1:], ". ")
	if domain == "" {
		return ""
	}
	if _, err := url.Parse("mailto:test@" + domain); err != nil {
		return ""
	}
	return domain
}

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

func ptrTime(t time.Time) *time.Time {
	return &t
}
