package message

import (
	"context"
	"errors"
	"fmt"
	"strings"

	"github.com/go-logr/logr"
	"github.com/go-logr/zapr"
	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	msgent "github.com/tidbcloud/devops/entity/message"
	msgmod "github.com/tidbcloud/devops/models/message"
	"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/service/message/api"
	msgrepo "github.com/tidbcloud/devops/pkg/service/message/repo"
	"github.com/tidbcloud/pkgv2/db"
	"go.uber.org/mock/gomock"
	"go.uber.org/zap"
	"go.uber.org/zap/zaptest/observer"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type fakeSendGridClient struct {
	bulkRequests []sendgridcli.SendBulkPersonalizedEmailsRequest
	bulkErrors   map[int]error
}

func (f *fakeSendGridClient) SendBulkPersonalizedEmails(_ context.Context, req sendgridcli.SendBulkPersonalizedEmailsRequest) error {
	req.To = append([]string(nil), req.To...)
	f.bulkRequests = append(f.bulkRequests, req)
	return f.bulkErrors[len(f.bulkRequests)]
}

func (f *fakeSendGridClient) SendPersonalizedEmail(context.Context, sendgridcli.SendPersonalizedEmailRequest) error {
	return nil
}

func (f *fakeSendGridClient) SendPlainEmail(context.Context, sendgridcli.SendPlainEmailRequest) (*sendgridcli.SendPlainEmailResponse, error) {
	return nil, nil
}

type failingTransactionProvider struct {
	db.TransProvider
	err error
}

func (p *failingTransactionProvider) Transaction(ctx context.Context, cb func(context.Context) error) error {
	return p.TransProvider.Transaction(ctx, func(ctx context.Context) error {
		if err := cb(ctx); err != nil {
			return err
		}
		return p.err
	})
}

func emailTarget(prefix string, count int) msgent.EmailTarget {
	emails := make([]string, count)
	for i := range emails {
		emails[i] = fmt.Sprintf("%s-%04d@example.com", prefix, i)
	}
	return msgent.EmailTarget{Users: &msgent.EmailTargetUsers{Emails: emails}}
}

func bulkEmailsParam(targets ...msgent.EmailTarget) api.SendBulkEmailsParam {
	return api.SendBulkEmailsParam{
		Name:    "bulk-test",
		Targets: targets,
		Template: msgent.EmailTemplate{
			ID:        "template-id",
			Variables: map[string]string{"body": "test"},
		},
		Reason:      "test bulk email",
		Applicant:   "applicant@pingcap.com",
		FromAddress: "no-reply@tidbcloud.com",
	}
}

var _ = Describe("SendBulkEmails", func() {
	var (
		gormDB   *gorm.DB
		provider db.TransProvider
		sgClient *fakeSendGridClient
		service  *svc
	)

	BeforeEach(func() {
		var err error
		gormDB, err = gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
		Expect(err).NotTo(HaveOccurred())
		sqlDB, err := gormDB.DB()
		Expect(err).NotTo(HaveOccurred())
		sqlDB.SetMaxOpenConns(1)
		Expect(gormDB.AutoMigrate(&msgmod.EmailRecord{})).To(Succeed())

		provider = db.NewProvider(gormDB)
		sgClient = &fakeSendGridClient{bulkErrors: map[int]error{}}
		service = &svc{
			repo:  msgrepo.NewRepo(provider),
			dbp:   provider,
			sgCli: sgClient,
		}
	})

	records := func() []*msgmod.EmailRecord {
		var result []*msgmod.EmailRecord
		Expect(gormDB.Order("id").Find(&result).Error).To(Succeed())
		return result
	}

	It("sends one batch and creates one record for each target", func() {
		targets := []msgent.EmailTarget{emailTarget("first", 600), emailTarget("second", 400)}

		successTargets, failedTargets, err := service.SendBulkEmails(context.Background(), bulkEmailsParam(targets...))

		Expect(err).NotTo(HaveOccurred())
		Expect(successTargets).To(Equal(targets))
		Expect(failedTargets).To(BeEmpty())
		Expect(sgClient.bulkRequests).To(HaveLen(1))
		Expect(sgClient.bulkRequests[0].To).To(HaveLen(1000))
		emailRecords := records()
		Expect(emailRecords).To(HaveLen(2))
		Expect(strings.Split(emailRecords[0].ToAddresses, ",")).To(HaveLen(600))
		Expect(strings.Split(emailRecords[1].ToAddresses, ",")).To(HaveLen(400))
		Expect(emailRecords[0].ToTarget).NotTo(BeEmpty())
		Expect(emailRecords[1].ToTarget).NotTo(BeEmpty())
	})

	It("starts a new batch instead of splitting a target", func() {
		targets := []msgent.EmailTarget{emailTarget("first", 600), emailTarget("second", 500)}

		successTargets, failedTargets, err := service.SendBulkEmails(context.Background(), bulkEmailsParam(targets...))

		Expect(err).NotTo(HaveOccurred())
		Expect(successTargets).To(Equal(targets))
		Expect(failedTargets).To(BeEmpty())
		Expect(sgClient.bulkRequests).To(HaveLen(2))
		Expect(sgClient.bulkRequests[0].To).To(HaveLen(600))
		Expect(sgClient.bulkRequests[1].To).To(HaveLen(500))
		Expect(records()).To(HaveLen(2))
	})

	It("accepts a target with exactly 1000 addresses", func() {
		target := emailTarget("maximum", 1000)

		successTargets, failedTargets, err := service.SendBulkEmails(context.Background(), bulkEmailsParam(target))

		Expect(err).NotTo(HaveOccurred())
		Expect(successTargets).To(Equal([]msgent.EmailTarget{target}))
		Expect(failedTargets).To(BeEmpty())
		Expect(sgClient.bulkRequests).To(HaveLen(1))
		Expect(sgClient.bulkRequests[0].To).To(HaveLen(1000))
		Expect(records()).To(HaveLen(1))
	})

	It("rejects a target with more than 1000 addresses before sending", func() {
		successTargets, failedTargets, err := service.SendBulkEmails(context.Background(), bulkEmailsParam(emailTarget("oversized", 1001)))

		Expect(err).To(MatchError("too many recipients for target: got 1001, maximum is 1000"))
		Expect(successTargets).To(BeEmpty())
		Expect(failedTargets).To(BeEmpty())
		Expect(sgClient.bulkRequests).To(BeEmpty())
		Expect(records()).To(BeEmpty())
	})

	It("returns an error without sending when target resolution fails", func() {
		resolveErr := errors.New("account service unavailable")
		accountClient := accountcli.NewMockClient(gomock.NewController(GinkgoT()))
		accountClient.EXPECT().GetTenantUsers(
			gomock.Any(),
			"tenant-id",
			gomock.Any(),
			"applicant@pingcap.com",
		).Return(nil, resolveErr)
		service.accountCli = accountClient
		param := bulkEmailsParam(
			emailTarget("resolved", 1),
			msgent.EmailTarget{Tenant: &msgent.EmailTargetTenant{TenantID: "tenant-id", Role: "Owner"}},
		)

		successTargets, failedTargets, err := service.SendBulkEmails(context.Background(), param)

		Expect(err).To(MatchError(ContainSubstring(resolveErr.Error())))
		Expect(successTargets).To(BeEmpty())
		Expect(failedTargets).To(BeEmpty())
		Expect(sgClient.bulkRequests).To(BeEmpty())
		Expect(records()).To(BeEmpty())
	})

	It("sends a duplicate address once and records every target", func() {
		core, observedLogs := observer.New(zap.InfoLevel)
		ctx := log.NewContext(context.Background(), zapr.NewLogger(zap.New(core)))
		first := msgent.EmailTarget{Users: &msgent.EmailTargetUsers{Emails: []string{"shared@example.com"}}}
		second := msgent.EmailTarget{Users: &msgent.EmailTargetUsers{Emails: []string{"shared@example.com"}}}

		successTargets, failedTargets, err := service.SendBulkEmails(ctx, bulkEmailsParam(first, second))

		Expect(err).NotTo(HaveOccurred())
		Expect(successTargets).To(Equal([]msgent.EmailTarget{first, second}))
		Expect(failedTargets).To(BeEmpty())
		Expect(sgClient.bulkRequests).To(HaveLen(1))
		Expect(sgClient.bulkRequests[0].To).To(Equal([]string{"shared@example.com"}))

		emailRecords := records()
		Expect(emailRecords).To(HaveLen(2))
		Expect(emailRecords[0].ToAddresses).To(Equal("shared@example.com"))
		Expect(emailRecords[1].ToAddresses).To(BeEmpty())
		secondRecord, err := service.GetEmailRecord(ctx, emailRecords[1].ID.Uint64())
		Expect(err).NotTo(HaveOccurred())
		Expect(secondRecord.ToTarget).To(Equal(&second))
		Expect(secondRecord.ToAddresses).To(Equal([]string{}))

		entries := observedLogs.FilterMessage("skip duplicate bulk email recipients").All()
		Expect(entries).To(HaveLen(1))
		fields := entries[0].ContextMap()
		Expect(fields).To(HaveKey("target"))
		Expect(fields["skipped_to_addresses"]).To(ConsistOf("shared@example.com"))
		Expect(fields["skipped_count"]).To(BeNumerically("==", 1))
	})

	It("records only the addresses actually sent for a partially duplicated target", func() {
		first := msgent.EmailTarget{Users: &msgent.EmailTargetUsers{Emails: []string{"shared@example.com", "first@example.com"}}}
		second := msgent.EmailTarget{Users: &msgent.EmailTargetUsers{Emails: []string{"shared@example.com", "second@example.com"}}}

		ctx := log.NewContext(context.Background(), logr.Discard())
		successTargets, failedTargets, err := service.SendBulkEmails(ctx, bulkEmailsParam(first, second))

		Expect(err).NotTo(HaveOccurred())
		Expect(successTargets).To(Equal([]msgent.EmailTarget{first, second}))
		Expect(failedTargets).To(BeEmpty())
		Expect(sgClient.bulkRequests).To(HaveLen(1))
		Expect(sgClient.bulkRequests[0].To).To(ConsistOf("shared@example.com", "first@example.com", "second@example.com"))
		emailRecords := records()
		Expect(strings.Split(emailRecords[0].ToAddresses, ",")).To(ConsistOf("shared@example.com", "first@example.com"))
		Expect(emailRecords[1].ToAddresses).To(Equal("second@example.com"))
	})

	It("deduplicates addresses across successful batches", func() {
		first := emailTarget("first", 600)
		sharedAddress := first.Users.Emails[0]
		second := emailTarget("second", 499)
		second.Users.Emails = append(second.Users.Emails, sharedAddress)

		ctx := log.NewContext(context.Background(), logr.Discard())
		successTargets, failedTargets, err := service.SendBulkEmails(ctx, bulkEmailsParam(first, second))

		Expect(err).NotTo(HaveOccurred())
		Expect(successTargets).To(Equal([]msgent.EmailTarget{first, second}))
		Expect(failedTargets).To(BeEmpty())
		Expect(sgClient.bulkRequests).To(HaveLen(2))
		Expect(sgClient.bulkRequests[0].To).To(HaveLen(600))
		Expect(sgClient.bulkRequests[1].To).To(HaveLen(499))
		Expect(sgClient.bulkRequests[1].To).NotTo(ContainElement(sharedAddress))
		Expect(strings.Split(records()[1].ToAddresses, ",")).NotTo(ContainElement(sharedAddress))
	})

	It("retries a duplicate address after an earlier batch fails", func() {
		first := emailTarget("first", 600)
		sharedAddress := first.Users.Emails[0]
		second := emailTarget("second", 499)
		second.Users.Emails = append(second.Users.Emails, sharedAddress)
		sgClient.bulkErrors[1] = errors.New("send failed")

		successTargets, failedTargets, err := service.SendBulkEmails(context.Background(), bulkEmailsParam(first, second))

		Expect(err).NotTo(HaveOccurred())
		Expect(successTargets).To(Equal([]msgent.EmailTarget{second}))
		Expect(failedTargets).To(Equal([]msgent.EmailTarget{first}))
		Expect(sgClient.bulkRequests).To(HaveLen(2))
		Expect(sgClient.bulkRequests[1].To).To(HaveLen(500))
		Expect(sgClient.bulkRequests[1].To).To(ContainElement(sharedAddress))
		Expect(strings.Split(records()[0].ToAddresses, ",")).To(ContainElement(sharedAddress))
	})

	It("skips targets without addresses", func() {
		empty := msgent.EmailTarget{Users: &msgent.EmailTargetUsers{}}
		nonEmpty := emailTarget("recipient", 1)

		successTargets, failedTargets, err := service.SendBulkEmails(context.Background(), bulkEmailsParam(empty, nonEmpty))

		Expect(err).NotTo(HaveOccurred())
		Expect(successTargets).To(Equal([]msgent.EmailTarget{nonEmpty}))
		Expect(failedTargets).To(Equal([]msgent.EmailTarget{empty}))
		Expect(sgClient.bulkRequests).To(HaveLen(1))
		Expect(records()).To(HaveLen(1))
	})

	It("does nothing when all targets are empty", func() {
		target := msgent.EmailTarget{}
		successTargets, failedTargets, err := service.SendBulkEmails(context.Background(), bulkEmailsParam(target))

		Expect(err).NotTo(HaveOccurred())
		Expect(successTargets).To(BeEmpty())
		Expect(failedTargets).To(Equal([]msgent.EmailTarget{target}))
		Expect(sgClient.bulkRequests).To(BeEmpty())
		Expect(records()).To(BeEmpty())
	})

	It("continues sending later batches when a batch fails", func() {
		targets := []msgent.EmailTarget{
			emailTarget("first", 600),
			emailTarget("second", 500),
			emailTarget("third", 600),
		}
		sendErr := errors.New("send failed")
		sgClient.bulkErrors[2] = sendErr

		successTargets, failedTargets, err := service.SendBulkEmails(context.Background(), bulkEmailsParam(targets...))

		Expect(err).NotTo(HaveOccurred())
		Expect(successTargets).To(Equal([]msgent.EmailTarget{targets[0], targets[2]}))
		Expect(failedTargets).To(Equal([]msgent.EmailTarget{targets[1]}))
		Expect(sgClient.bulkRequests).To(HaveLen(3))
		Expect(records()).To(HaveLen(2))
	})

	It("continues sending when the first batch fails", func() {
		targets := []msgent.EmailTarget{
			emailTarget("first", 600),
			{Users: &msgent.EmailTargetUsers{}},
			emailTarget("second", 500),
		}
		sgClient.bulkErrors[1] = errors.New("send failed")

		successTargets, failedTargets, err := service.SendBulkEmails(context.Background(), bulkEmailsParam(targets...))

		Expect(err).NotTo(HaveOccurred())
		Expect(successTargets).To(Equal([]msgent.EmailTarget{targets[2]}))
		Expect(failedTargets).To(Equal(targets[:2]))
		Expect(sgClient.bulkRequests).To(HaveLen(2))
		Expect(records()).To(HaveLen(1))
	})

	It("returns targets sent before the transaction rolls back", func() {
		commitErr := errors.New("commit failed")
		service.dbp = &failingTransactionProvider{TransProvider: provider, err: commitErr}
		target := emailTarget("recipient", 1)

		successTargets, failedTargets, err := service.SendBulkEmails(context.Background(), bulkEmailsParam(target))

		Expect(err).NotTo(HaveOccurred())
		Expect(successTargets).To(Equal([]msgent.EmailTarget{target}))
		Expect(failedTargets).To(BeEmpty())
		Expect(sgClient.bulkRequests).To(HaveLen(1))
		Expect(records()).To(BeEmpty())
	})

	It("returns the batch as failed when record creation fails", func() {
		param := bulkEmailsParam(emailTarget("recipient", 1))
		param.FromAddress = "invalid@example.com"

		successTargets, failedTargets, err := service.SendBulkEmails(context.Background(), param)

		Expect(err).NotTo(HaveOccurred())
		Expect(successTargets).To(BeEmpty())
		Expect(failedTargets).To(Equal(param.Targets))
		Expect(sgClient.bulkRequests).To(BeEmpty())
		Expect(records()).To(BeEmpty())
	})

	It("keeps SendEmail validation for an empty resolved target", func() {
		err := service.SendEmail(context.Background(), api.SendEmailParam{
			Target:      msgent.EmailTarget{},
			Template:    msgent.EmailTemplate{ID: "template-id"},
			Reason:      "test email",
			Applicant:   "applicant@pingcap.com",
			FromAddress: "no-reply@tidbcloud.com",
		})

		Expect(err).To(MatchError(`field "to_addresses" error: is required`))
		Expect(records()).To(BeEmpty())
	})
})
