package queue

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"net/mail"
	"strings"
	"time"

	"wucher/internal/shared/pkg/uuidv7"
)

const (
	BackendSQS = "sqs"

	QueueTypeStandard = "standard"
	QueueTypeFIFO     = "fifo"

	EmailKind = "email.dispatch"
)

var (
	ErrInvalidEmailJob = errors.New("invalid email job")
)

type EmailJob struct {
	JobID        string            `json:"job_id,omitempty"`
	MessageType  string            `json:"message_type,omitempty"`
	To           string            `json:"to"`
	CC           []string          `json:"cc,omitempty"`
	BCC          []string          `json:"bcc,omitempty"`
	Subject      string            `json:"subject"`
	TemplateName string            `json:"template_name,omitempty"`
	TemplateData map[string]any    `json:"template_data,omitempty"`
	TextBody     string            `json:"text_body,omitempty"`
	HTMLBody     string            `json:"html_body,omitempty"`
	Attachments  []EmailAttachment `json:"attachments,omitempty"`
	ReplyTo      []string          `json:"reply_to,omitempty"`
	Tags         map[string]string `json:"tags,omitempty"`
	Metadata     map[string]string `json:"metadata,omitempty"`
	CreatedAt    time.Time         `json:"created_at,omitempty"`
	TraceID      string            `json:"trace_id,omitempty"`
	RequestID    string            `json:"request_id,omitempty"`

	// Legacy aliases kept for backward-compatible decoding and tests.
	Type string `json:"type,omitempty"`
	Body string `json:"body,omitempty"`
}

type EmailAttachment struct {
	Filename    string `json:"filename,omitempty"`
	ContentType string `json:"content_type,omitempty"`
	ContentID   string `json:"content_id,omitempty"`
	Content     []byte `json:"content,omitempty"`
	Inline      bool   `json:"inline,omitempty"`
}

func (j EmailJob) Validate() error {
	job := j.Canonical()
	if strings.TrimSpace(job.To) == "" {
		return errors.Join(ErrInvalidEmailJob, errors.New("recipient is required"))
	}
	if err := validateEmailAddress(job.To); err != nil {
		return errors.Join(ErrInvalidEmailJob, err)
	}
	if err := validateEmailAddresses(job.CC); err != nil {
		return errors.Join(ErrInvalidEmailJob, err)
	}
	if err := validateEmailAddresses(job.BCC); err != nil {
		return errors.Join(ErrInvalidEmailJob, err)
	}
	if err := validateEmailAddresses(job.ReplyTo); err != nil {
		return errors.Join(ErrInvalidEmailJob, err)
	}
	hasBody := strings.TrimSpace(job.TextBody) != "" || strings.TrimSpace(job.HTMLBody) != ""
	if strings.TrimSpace(job.Subject) == "" && (strings.TrimSpace(job.TemplateName) == "" || hasBody) {
		return errors.Join(ErrInvalidEmailJob, errors.New("subject is required"))
	}
	if !hasBody && strings.TrimSpace(job.TemplateName) == "" {
		return errors.Join(ErrInvalidEmailJob, errors.New("body is required"))
	}
	for i := range job.Attachments {
		attachment := job.Attachments[i]
		if len(attachment.Content) == 0 {
			return errors.Join(ErrInvalidEmailJob, errors.New("attachment content is required"))
		}
		if strings.TrimSpace(attachment.ContentType) == "" {
			return errors.Join(ErrInvalidEmailJob, errors.New("attachment content type is required"))
		}
		if attachment.Inline && strings.TrimSpace(attachment.ContentID) == "" {
			return errors.Join(ErrInvalidEmailJob, errors.New("inline attachment content id is required"))
		}
	}
	return nil
}

func (j EmailJob) RecipientHash() string {
	sum := sha256.Sum256([]byte(strings.ToLower(strings.TrimSpace(j.Canonical().To))))
	return hex.EncodeToString(sum[:8])
}

func (j EmailJob) NormalizedType() string {
	if v := strings.TrimSpace(j.Canonical().MessageType); v != "" {
		return v
	}
	return "generic"
}

func (j EmailJob) TextBodyValue() string {
	return j.Canonical().TextBody
}

func (j EmailJob) Canonical() EmailJob {
	job := j
	job.To = strings.TrimSpace(job.To)
	job.Subject = strings.TrimSpace(job.Subject)
	job.TemplateName = strings.TrimSpace(job.TemplateName)
	job.JobID = strings.TrimSpace(job.JobID)
	job.TraceID = strings.TrimSpace(job.TraceID)
	job.RequestID = strings.TrimSpace(job.RequestID)
	if strings.TrimSpace(job.MessageType) == "" {
		job.MessageType = strings.TrimSpace(job.Type)
	}
	if strings.TrimSpace(job.TextBody) == "" {
		job.TextBody = job.Body
	}
	job.Type = ""
	job.Body = ""
	job.CC = cleanStrings(job.CC)
	job.BCC = cleanStrings(job.BCC)
	job.ReplyTo = cleanStrings(job.ReplyTo)
	job.Tags = cleanStringMap(job.Tags)
	job.Metadata = cleanStringMap(job.Metadata)
	job.Attachments = canonicalEmailAttachments(job.Attachments)
	return job
}

func canonicalEmailAttachments(values []EmailAttachment) []EmailAttachment {
	if len(values) == 0 {
		return nil
	}
	out := make([]EmailAttachment, 0, len(values))
	for i := range values {
		item := values[i]
		item.Filename = strings.TrimSpace(item.Filename)
		item.ContentType = strings.TrimSpace(item.ContentType)
		item.ContentID = strings.Trim(strings.TrimSpace(item.ContentID), "<>")
		if len(item.Content) == 0 {
			continue
		}
		item.Content = append([]byte(nil), item.Content...)
		out = append(out, item)
	}
	if len(out) == 0 {
		return nil
	}
	return out
}

type Envelope struct {
	Version        string    `json:"version"`
	MessageID      string    `json:"message_id"`
	Kind           string    `json:"kind"`
	OccurredAt     time.Time `json:"occurred_at"`
	CorrelationID  string    `json:"correlation_id,omitempty"`
	TraceID        string    `json:"trace_id,omitempty"`
	IdempotencyKey string    `json:"idempotency_key,omitempty"`
	Payload        EmailJob  `json:"payload"`
}

func NewEnvelope(ctx context.Context, version string, job EmailJob) (*Envelope, error) {
	job = job.Canonical()
	if err := job.Validate(); err != nil {
		return nil, err
	}
	id, err := newMessageID()
	if err != nil {
		return nil, err
	}
	correlationID := CorrelationIDFromContext(ctx)
	if correlationID == "" {
		correlationID = id
	}
	traceID := TraceIDFromContext(ctx)
	if traceID == "" {
		traceID = correlationID
	}
	now := time.Now().UTC()
	if job.JobID == "" {
		job.JobID = id
	}
	if job.CreatedAt.IsZero() {
		job.CreatedAt = now
	}
	if job.TraceID == "" {
		job.TraceID = traceID
	}
	if job.RequestID == "" {
		job.RequestID = correlationID
	}
	return &Envelope{
		Version:        strings.TrimSpace(version),
		MessageID:      id,
		Kind:           EmailKind,
		OccurredAt:     now,
		CorrelationID:  correlationID,
		TraceID:        traceID,
		IdempotencyKey: job.JobID,
		Payload:        job,
	}, nil
}

func (e Envelope) DeduplicationKey() string {
	if v := strings.TrimSpace(e.IdempotencyKey); v != "" {
		return v
	}
	return strings.TrimSpace(e.MessageID)
}

type OutboundMessage struct {
	ID              string
	Body            []byte
	Attributes      map[string]string
	MessageGroupID  string
	DeduplicationID string
}

type Delivery struct {
	ID            string
	ReceiptHandle string
	Body          []byte
	Attributes    map[string]string
	ReceiveCount  int
	EnqueuedAt    time.Time
}

type MessageSerializer interface {
	Serialize(ctx context.Context, job EmailJob) (*OutboundMessage, error)
	Deserialize(body []byte, attributes map[string]string) (*Envelope, error)
}

type QueueProducer interface {
	Backend() string
	Publish(ctx context.Context, message OutboundMessage) error
}

type QueueConsumer interface {
	Backend() string
	Receive(ctx context.Context, maxMessages int) ([]*Delivery, error)
	Ack(ctx context.Context, deliveries []*Delivery) error
	Retry(ctx context.Context, delivery *Delivery, delay time.Duration) error
	DeadLetter(ctx context.Context, delivery *Delivery, reason string, cause error) error
	ApproximateDepth(ctx context.Context) (int64, error)
	Close() error
}

type EmailJobProducer interface {
	Enqueue(ctx context.Context, job EmailJob) error
}

type EmailJobHandler interface {
	Handle(ctx context.Context, envelope *Envelope) error
}

func newMessageID() (string, error) {
	id, err := uuidv7.String(uuidv7.MustNew())
	if err != nil {
		return "", err
	}
	return id, nil
}

func cleanStrings(values []string) []string {
	if len(values) == 0 {
		return nil
	}
	out := make([]string, 0, len(values))
	for _, value := range values {
		value = strings.TrimSpace(value)
		if value == "" {
			continue
		}
		out = append(out, value)
	}
	if len(out) == 0 {
		return nil
	}
	return out
}

func cleanStringMap(values map[string]string) map[string]string {
	if len(values) == 0 {
		return nil
	}
	out := make(map[string]string, len(values))
	for key, value := range values {
		key = strings.TrimSpace(key)
		value = strings.TrimSpace(value)
		if key == "" || value == "" {
			continue
		}
		out[key] = value
	}
	if len(out) == 0 {
		return nil
	}
	return out
}

func validateEmailAddresses(addresses []string) error {
	for _, address := range addresses {
		if err := validateEmailAddress(address); err != nil {
			return err
		}
	}
	return nil
}

func validateEmailAddress(address string) error {
	address = strings.TrimSpace(address)
	if address == "" {
		return nil
	}
	if _, err := mail.ParseAddress(address); err != nil {
		return err
	}
	return nil
}
