package service

import (
	"context"
	"encoding/json"
	"errors"
	"net/mail"
	"net/url"
	"sort"
	"strings"
	"time"

	awsv2 "github.com/aws/aws-sdk-go-v2/aws"
	awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware"
	awsconfig "github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/service/sesv2"
	sestypes "github.com/aws/aws-sdk-go-v2/service/sesv2/types"

	"wucher/internal/config"
	"wucher/internal/queue"
	"wucher/internal/resilience"
	"wucher/internal/shared/pkg/metrics"
)

const utf8Charset = "UTF-8"

type sesAPI interface {
	SendEmail(ctx context.Context, params *sesv2.SendEmailInput, optFns ...func(*sesv2.Options)) (*sesv2.SendEmailOutput, error)
}

type SESSender struct {
	cfg       config.SESConfig
	api       sesAPI
	executor  *resilience.Executor
	component string
}

func NewSESSender(ctx context.Context, cfg config.SESConfig) (*SESSender, error) {
	api, err := newSESAPI(ctx, cfg)
	if err != nil {
		return nil, err
	}
	return &SESSender{
		cfg:       cfg,
		api:       api,
		component: "worker",
	}, nil
}

func NewSESSenderWithAPI(cfg config.SESConfig, api sesAPI) *SESSender {
	return &SESSender{
		cfg:       cfg,
		api:       api,
		component: "worker",
	}
}

func (s *SESSender) WithExecutor(executor *resilience.Executor) *SESSender {
	if s == nil {
		return nil
	}
	s.executor = executor
	return s
}

func (s *SESSender) WithMetricsComponent(component string) *SESSender {
	if s == nil {
		return nil
	}
	component = strings.TrimSpace(component)
	if component == "" {
		return s
	}
	s.component = component
	return s
}

func (s *SESSender) Configured() bool {
	return s != nil && strings.TrimSpace(s.cfg.Region) != "" && strings.TrimSpace(s.cfg.From) != ""
}

func (s *SESSender) Send(ctx context.Context, job queue.EmailJob) (EmailDeliveryResult, error) {
	return resilience.Do(ctx, s.executor, func(ctx context.Context) (EmailDeliveryResult, error) {
		if !s.Configured() {
			return EmailDeliveryResult{}, errors.New("ses is not configured")
		}
		if s.api == nil {
			return EmailDeliveryResult{}, errors.New("ses client is required")
		}
		job = job.Canonical()
		if err := job.Validate(); err != nil {
			return EmailDeliveryResult{}, err
		}
		input, err := s.buildSendInput(job)
		if err != nil {
			return EmailDeliveryResult{}, err
		}

		sendCtx := ctx
		cancel := func() {}
		if s.cfg.SendTimeout > 0 {
			sendCtx, cancel = context.WithTimeout(ctx, s.cfg.SendTimeout)
		}
		defer cancel()

		started := time.Now()
		output, err := s.api.SendEmail(sendCtx, input)
		metrics.ObserveAWSRequest(s.component, "ses", "SendEmail", err, started)
		metrics.ObserveEmailSend(s.component, "ses", err, started)
		if err != nil {
			return EmailDeliveryResult{}, err
		}

		result := EmailDeliveryResult{
			Provider:          "ses",
			ProviderMessageID: awsv2.ToString(output.MessageId),
		}
		if requestID, ok := awsmiddleware.GetRequestIDMetadata(output.ResultMetadata); ok {
			result.RequestID = requestID
		}
		return result, nil
	})
}

func (s *SESSender) buildSendInput(job queue.EmailJob) (*sesv2.SendEmailInput, error) {
	from, err := mail.ParseAddress(strings.TrimSpace(s.cfg.From))
	if err != nil {
		return nil, err
	}
	to, err := normalizeEmailAddresses([]string{job.To})
	if err != nil {
		return nil, err
	}
	cc, err := normalizeEmailAddresses(job.CC)
	if err != nil {
		return nil, err
	}
	bcc, err := normalizeEmailAddresses(job.BCC)
	if err != nil {
		return nil, err
	}
	replyTo, err := normalizeEmailAddresses(job.ReplyTo)
	if err != nil {
		return nil, err
	}

	input := &sesv2.SendEmailInput{
		FromEmailAddress: awsv2.String(from.String()),
		Destination: &sestypes.Destination{
			ToAddresses:  to,
			CcAddresses:  cc,
			BccAddresses: bcc,
		},
		ReplyToAddresses: replyTo,
		EmailTags:        buildSESTags(job.Tags),
	}
	if cfgSet := strings.TrimSpace(s.cfg.ConfigurationSetName); cfgSet != "" {
		input.ConfigurationSetName = awsv2.String(cfgSet)
	}

	content, err := buildSESContent(from.String(), to, cc, replyTo, job)
	if err != nil {
		return nil, err
	}
	input.Content = content
	return input, nil
}

func buildSESContent(from string, to, cc, replyTo []string, job queue.EmailJob) (*sestypes.EmailContent, error) {
	if len(job.Attachments) > 0 {
		return buildSESRawContent(from, to, cc, replyTo, job)
	}
	if strings.TrimSpace(job.TemplateName) != "" && strings.TrimSpace(job.TextBodyValue()) == "" && strings.TrimSpace(job.HTMLBody) == "" {
		templateData := "{}"
		if len(job.TemplateData) > 0 {
			encoded, err := json.Marshal(job.TemplateData)
			if err != nil {
				return nil, err
			}
			templateData = string(encoded)
		}
		return &sestypes.EmailContent{
			Template: &sestypes.Template{
				TemplateName: awsv2.String(job.TemplateName),
				TemplateData: awsv2.String(templateData),
			},
		}, nil
	}

	body := &sestypes.Body{}
	if textBody := strings.TrimSpace(job.TextBodyValue()); textBody != "" {
		body.Text = &sestypes.Content{
			Data:    awsv2.String(textBody),
			Charset: awsv2.String(utf8Charset),
		}
	}
	if htmlBody := strings.TrimSpace(job.HTMLBody); htmlBody != "" {
		body.Html = &sestypes.Content{
			Data:    awsv2.String(htmlBody),
			Charset: awsv2.String(utf8Charset),
		}
	}
	return &sestypes.EmailContent{
		Simple: &sestypes.Message{
			Subject: &sestypes.Content{
				Data:    awsv2.String(sanitizeSubject(job.Subject)),
				Charset: awsv2.String(utf8Charset),
			},
			Body: body,
		},
	}, nil
}

func normalizeEmailAddresses(addresses []string) ([]string, error) {
	if len(addresses) == 0 {
		return nil, nil
	}
	out := make([]string, 0, len(addresses))
	for _, address := range addresses {
		address = strings.TrimSpace(address)
		if address == "" {
			continue
		}
		parsed, err := mail.ParseAddress(address)
		if err != nil {
			return nil, err
		}
		out = append(out, parsed.Address)
	}
	if len(out) == 0 {
		return nil, nil
	}
	return out, nil
}

func buildSESTags(tags map[string]string) []sestypes.MessageTag {
	if len(tags) == 0 {
		return nil
	}
	keys := make([]string, 0, len(tags))
	for key := range tags {
		keys = append(keys, key)
	}
	sort.Strings(keys)
	out := make([]sestypes.MessageTag, 0, len(keys))
	for _, key := range keys {
		value := strings.TrimSpace(tags[key])
		if strings.TrimSpace(key) == "" || value == "" {
			continue
		}
		out = append(out, sestypes.MessageTag{
			Name:  awsv2.String(strings.TrimSpace(key)),
			Value: awsv2.String(value),
		})
	}
	if len(out) == 0 {
		return nil
	}
	return out
}

func sanitizeSubject(subject string) string {
	subject = strings.TrimSpace(subject)
	subject = strings.ReplaceAll(subject, "\r", "")
	subject = strings.ReplaceAll(subject, "\n", "")
	return subject
}

func newSESAPI(ctx context.Context, cfg config.SESConfig) (sesAPI, error) {
	if err := validateSESConfig(cfg); err != nil {
		return nil, err
	}
	loadOptions := make([]func(*awsconfig.LoadOptions) error, 0, 1)
	if strings.TrimSpace(cfg.Region) != "" {
		loadOptions = append(loadOptions, awsconfig.WithRegion(strings.TrimSpace(cfg.Region)))
	}
	awsCfg, err := awsconfig.LoadDefaultConfig(ctx, loadOptions...)
	if err != nil {
		return nil, err
	}
	return sesv2.NewFromConfig(awsCfg, func(options *sesv2.Options) {
		if endpoint := strings.TrimSpace(cfg.Endpoint); endpoint != "" {
			options.EndpointResolver = sesv2.EndpointResolverFromURL(endpoint)
		}
	}), nil
}

func validateSESConfig(cfg config.SESConfig) error {
	if strings.TrimSpace(cfg.Region) == "" {
		return errors.New("aws region is required")
	}
	if strings.TrimSpace(cfg.From) == "" {
		return errors.New("ses from address is required")
	}
	if endpoint := strings.TrimSpace(cfg.Endpoint); endpoint != "" {
		parsed, err := url.Parse(endpoint)
		if err != nil {
			return err
		}
		if parsed.Scheme != "https" && !cfg.AllowInsecureEndpoint {
			return errors.New("insecure ses endpoint is disabled")
		}
	}
	return nil
}
