package message

import (
	"context"
	"fmt"
	"time"

	msgent "github.com/tidbcloud/devops/entity/message"
	"github.com/tidbcloud/devops/entity/validation"
	"github.com/tidbcloud/devops/pkg/infra/clients/accountcli"
	"github.com/tidbcloud/devops/pkg/infra/clients/sendgridcli"
	"github.com/tidbcloud/devops/pkg/infra/log"
	"github.com/tidbcloud/devops/pkg/infra/utils/container"
	"github.com/tidbcloud/devops/pkg/infra/utils/repoopt"
	"github.com/tidbcloud/devops/pkg/service/message/api"
)

func (s *svc) SendEmail(ctx context.Context, param api.SendEmailParam) error {
	newEmailRecord := &msgent.EmailRecord{
		ID:            0,
		Name:          param.Name,
		Reason:        param.Reason,
		Applicant:     param.Applicant,
		ToTarget:      &param.Target,
		EmailTemplate: param.Template,
		FromAddress:   param.FromAddress,
		CCAddresses:   param.CCAdresses,
		BCCAddresses:  param.BCCAdresses,
	}

	addrs, err := s.buildAddresses(ctx, newEmailRecord.Applicant, *newEmailRecord.ToTarget)
	if err != nil {
		return fmt.Errorf("build to addresses failed: %w", err)
	}
	newEmailRecord.ToAddresses = addrs
	if len(newEmailRecord.ToAddresses) == 0 {
		return validation.ErrRequired("to_addresses")
	}

	return s.dbp.Transaction(ctx, func(ctx context.Context) error {
		var err error

		// record in db
		newEmailRecord, err = s.repo.EmailRecord.Create(ctx, newEmailRecord)
		if err != nil {
			return err
		}

		// send via sendgrid
		req := sendgridcli.SendPersonalizedEmailRequest{
			From:         newEmailRecord.FromAddress,
			To:           newEmailRecord.ToAddresses,
			CC:           newEmailRecord.CCAddresses,
			BCC:          newEmailRecord.BCCAddresses,
			TemplateID:   newEmailRecord.EmailTemplate.ID,
			TemplateVars: newEmailRecord.EmailTemplate.Variables,
			DeliveryTime: time.Now(),
		}
		err = s.sgCli.SendPersonalizedEmail(ctx, req)
		if err != nil {
			return err
		}

		return nil
	})
}

type bulkEmailTarget struct {
	target      msgent.EmailTarget
	toAddresses []string
}

func (s *svc) SendBulkEmails(
	ctx context.Context,
	param api.SendBulkEmailsParam,
) ([]msgent.EmailTarget, []msgent.EmailTarget, error) {
	successTargets := make([]msgent.EmailTarget, 0, len(param.Targets))
	failedTargets := make([]msgent.EmailTarget, 0, len(param.Targets))
	targets := make([]bulkEmailTarget, 0, len(param.Targets))
	for _, target := range param.Targets {
		toAddresses, err := s.buildAddresses(ctx, param.Applicant, target)
		if err != nil {
			return nil, nil, fmt.Errorf("build to addresses failed: %w", err)
		}
		if len(toAddresses) > sendgridcli.MaxBulkPersonalizedEmailRecipients {
			return nil, nil, fmt.Errorf(
				"too many recipients for target: got %d, maximum is %d",
				len(toAddresses),
				sendgridcli.MaxBulkPersonalizedEmailRecipients,
			)
		}
		targets = append(targets, bulkEmailTarget{
			target:      target,
			toAddresses: toAddresses,
		})
	}

	sentAddresses := container.NewSet[string]()
	batch := make([]bulkEmailTarget, 0, len(targets))
	for start := 0; start < len(targets); {
		end := start
		toAddressCount := 0
		batch = batch[:0]
		batchAddresses := container.NewSet[string]()

		for end < len(targets) {
			if len(targets[end].toAddresses) == 0 {
				end++
				continue
			}

			toAddresses := make([]string, 0, len(targets[end].toAddresses))
			duplicateAddresses := make([]string, 0)
			for _, address := range targets[end].toAddresses {
				if sentAddresses.Has(address) || batchAddresses.Has(address) {
					duplicateAddresses = append(duplicateAddresses, address)
					continue
				}
				toAddresses = append(toAddresses, address)
			}
			if toAddressCount+len(toAddresses) > sendgridcli.MaxBulkPersonalizedEmailRecipients {
				break
			}

			batch = append(batch, bulkEmailTarget{
				target:      targets[end].target,
				toAddresses: toAddresses,
			})
			batchAddresses.Insert(toAddresses...)
			toAddressCount += len(toAddresses)
			if len(duplicateAddresses) > 0 {
				log.FromContext(ctx).Info(
					"skip duplicate bulk email recipients",
					"target", targets[end].target,
					"skipped_to_addresses", duplicateAddresses,
					"skipped_count", len(duplicateAddresses),
				)
			}
			end++
		}

		wasSent := false
		var err error
		if len(batch) > 0 {
			wasSent, err = s.sendBulkEmailBatch(ctx, param, batch, toAddressCount)
		}
		if wasSent {
			sentAddresses.Insert(batchAddresses.Slice()...)
		}
		wasSuccessful := wasSent || err == nil
		for _, target := range targets[start:end] {
			if len(target.toAddresses) == 0 {
				failedTargets = append(failedTargets, target.target)
			} else if wasSuccessful {
				successTargets = append(successTargets, target.target)
			} else {
				failedTargets = append(failedTargets, target.target)
			}
		}
		if err != nil {
			log.FromContext(ctx).Error(err, "failed to send bulk email batch")
		}
		start = end
	}

	return successTargets, failedTargets, nil
}

func (s *svc) sendBulkEmailBatch(
	ctx context.Context,
	param api.SendBulkEmailsParam,
	targets []bulkEmailTarget,
	toAddressCount int,
) (bool, error) {
	wasSent := false
	err := s.dbp.Transaction(ctx, func(ctx context.Context) error {
		toAddresses := make([]string, 0, toAddressCount)
		for _, target := range targets {
			emailTarget := target.target
			record := &msgent.EmailRecord{
				Name:          param.Name,
				Reason:        param.Reason,
				Applicant:     param.Applicant,
				ToTarget:      &emailTarget,
				EmailTemplate: param.Template,
				FromAddress:   param.FromAddress,
				ToAddresses:   target.toAddresses,
			}
			if _, err := s.repo.EmailRecord.Create(ctx, record); err != nil {
				return err
			}
			toAddresses = append(toAddresses, target.toAddresses...)
		}

		if len(toAddresses) > 0 {
			err := s.sgCli.SendBulkPersonalizedEmails(ctx, sendgridcli.SendBulkPersonalizedEmailsRequest{
				From:         param.FromAddress,
				To:           toAddresses,
				TemplateID:   param.Template.ID,
				TemplateVars: param.Template.Variables,
				DeliveryTime: time.Now(),
			})
			if err != nil {
				return err
			}
			wasSent = true
		}
		return nil
	})
	return wasSent, err
}

func (s *svc) ListEmailRecords(ctx context.Context, param api.ListEmailRecordsParam) ([]*msgent.EmailRecord, int64, error) {
	optSet := s.repo.EmailRecord.Options()
	opts := []repoopt.Option{}

	if param.ReasonLike != nil {
		opts = append(opts, optSet.ReasonLike(*param.ReasonLike))
	}
	if param.FromAddressIs != nil {
		opts = append(opts, optSet.FromAddressIs(*param.FromAddressIs))
	}
	if param.NameIs != nil {
		opts = append(opts, optSet.NameIs(*param.NameIs))
	}

	total, err := s.repo.EmailRecord.Count(ctx, opts...)
	if err != nil {
		return nil, 0, err
	}

	if param.Page > 0 {
		opts = append(opts, optSet.WithPager(param.Page, param.PerPage))
	}

	records, err := s.repo.EmailRecord.List(ctx, opts...)
	if err != nil {
		return nil, 0, err
	}

	return records, total, nil
}

func (s *svc) GetEmailRecord(ctx context.Context, id uint64) (*msgent.EmailRecord, error) {
	return s.repo.EmailRecord.Get(ctx, id)
}

func (s *svc) buildAddresses(ctx context.Context, applicant string, target msgent.EmailTarget) ([]string, error) {
	addrs := container.NewSet[string]()

	if target.Users != nil {
		addrs.Insert(target.Users.Emails...)
	}

	if target.Tenant != nil {
		page := int64(1)
		for {
			req := accountcli.GetUsersReq{
				Page:      page,
				PageSize:  50,
				FullEmail: true,
			}
			resp, err := s.accountCli.GetTenantUsers(ctx, target.Tenant.TenantID, req, applicant)
			if err != nil {
				return nil, fmt.Errorf("query user of tenant '%s' failed: %v", target.Tenant.TenantID, err)
			}

			page++
			if len(resp.Items) == 0 {
				break
			}

			for _, user := range resp.Items {
				if user.Status == accountcli.UserStatusActive && user.Role == target.Tenant.Role {
					addrs.Insert(user.Email)
				}
			}
		}

	}

	return addrs.Slice(), nil
}
