package mailer

import (
	"context"
	"fmt"
	"log/slog"
	"net/http"
	"net/url"
	"strconv"
	"strings"
	"time"

	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"
)

const emailCharset = "UTF-8"

// Mailer sends verification and invite emails.
type Mailer interface {
	SendVerificationEmail(ctx context.Context, email string, token string) error
	SendPasswordResetEmail(ctx context.Context, email string, token string) error
	SendWorkspaceInviteEmail(ctx context.Context, email string, workspaceName string, token string) error
	SendOrganizationInviteEmail(ctx context.Context, email string, organizationName string, token string) error
}

type noopMailer struct{}

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

type sesMailer struct {
	client                  sesClient
	fromAddress             string
	publicBaseURL           string
	productionAccessEnabled *bool
}

type SESOption func(*sesOptions)

type sesOptions struct {
	httpClient *http.Client
}

func WithSESHTTPClient(httpClient *http.Client) SESOption {
	return func(options *sesOptions) {
		options.httpClient = httpClient
	}
}

// NewNoop constructs a silent mailer. Development and E2E callers that need
// delivery credentials must wrap it with a dedicated mailbox instead of
// writing credentials to the operational logger.
func NewNoop(_ *slog.Logger, _ string) Mailer {
	return &noopMailer{}
}

// NewSES constructs an Amazon SES-backed mailer.
func NewSES(ctx context.Context, publicBaseURL string, fromAddress string, region string, opts ...SESOption) (Mailer, error) {
	trimmedFromAddress := strings.TrimSpace(fromAddress)
	if trimmedFromAddress == "" {
		return nil, fmt.Errorf("mail from address is required")
	}
	trimmedRegion := strings.TrimSpace(region)
	if trimmedRegion == "" {
		return nil, fmt.Errorf("mail SES region is required")
	}

	options := sesOptions{}
	for _, opt := range opts {
		if opt != nil {
			opt(&options)
		}
	}

	loadOptions := []func(*awsconfig.LoadOptions) error{awsconfig.WithRegion(trimmedRegion)}
	if options.httpClient != nil {
		loadOptions = append(loadOptions, awsconfig.WithHTTPClient(options.httpClient))
	}
	cfg, err := awsconfig.LoadDefaultConfig(ctx, loadOptions...)
	if err != nil {
		return nil, fmt.Errorf("load AWS config: %w", err)
	}

	client := sesv2.NewFromConfig(cfg)

	var productionAccessEnabled *bool
	accountCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
	defer cancel()
	account, err := client.GetAccount(accountCtx, &sesv2.GetAccountInput{})
	if err == nil {
		enabled := account.ProductionAccessEnabled
		productionAccessEnabled = &enabled
	}

	return newSESMailer(publicBaseURL, trimmedFromAddress, client, productionAccessEnabled), nil
}

func newSESMailer(publicBaseURL string, fromAddress string, client sesClient, productionAccessEnabled *bool) *sesMailer {
	return &sesMailer{
		client:                  client,
		fromAddress:             strings.TrimSpace(fromAddress),
		publicBaseURL:           publicBaseURL,
		productionAccessEnabled: productionAccessEnabled,
	}
}

func (mailer *noopMailer) SendVerificationEmail(_ context.Context, _ string, _ string) error {
	return nil
}

func (mailer *noopMailer) SendPasswordResetEmail(_ context.Context, _ string, _ string) error {
	return nil
}

func (mailer *noopMailer) SendWorkspaceInviteEmail(_ context.Context, _ string, _ string, _ string) error {
	return nil
}

func (mailer *noopMailer) SendOrganizationInviteEmail(_ context.Context, _ string, _ string, _ string) error {
	return nil
}

func (mailer *sesMailer) SendVerificationEmail(ctx context.Context, email string, token string) error {
	body := strings.Join([]string{
		"Welcome to AgentTeam.",
		"Use the verification token below to finish verifying your email:",
		token,
		"You can also open this link:",
		tokenLink(mailer.publicBaseURL, "verify-email", token),
		"If you did not request this email, you can safely ignore it.",
	}, "\n\n")
	return mailer.sendPlainTextEmail(ctx, email, "Verify your AgentTeam email", body)
}

func (mailer *sesMailer) SendPasswordResetEmail(ctx context.Context, email string, token string) error {
	body := strings.Join([]string{
		"A password reset was requested for your AgentTeam account.",
		"Use the reset token below to set a new password:",
		token,
		"You can also open this link:",
		tokenLink(mailer.publicBaseURL, "reset-password", token),
		"If you did not request a password reset, you can safely ignore this email.",
	}, "\n\n")
	return mailer.sendPlainTextEmail(ctx, email, "Reset your AgentTeam password", body)
}

func (mailer *sesMailer) SendWorkspaceInviteEmail(ctx context.Context, email string, workspaceName string, token string) error {
	body := strings.Join([]string{
		fmt.Sprintf("You've been invited to join %s on AgentTeam.", strings.TrimSpace(workspaceName)),
		"Use the invite token below after you register and verify your email:",
		token,
		"You can also open this link:",
		tokenLink(mailer.publicBaseURL, "accept-invite", token),
		"If you were not expecting this invite, you can safely ignore it.",
	}, "\n\n")
	return mailer.sendPlainTextEmail(ctx, email, "Your AgentTeam workspace invite", body)
}

func (mailer *sesMailer) SendOrganizationInviteEmail(ctx context.Context, email string, organizationName string, token string) error {
	body := strings.Join([]string{
		fmt.Sprintf("You've been invited to join %s on AgentTeam.", strings.TrimSpace(organizationName)),
		"Use the invite token below after you register and verify your email:",
		token,
		"You can also open this link:",
		tokenLink(mailer.publicBaseURL, "accept-invite", token),
		"If you were not expecting this invite, you can safely ignore it.",
	}, "\n\n")
	return mailer.sendPlainTextEmail(ctx, email, "Your AgentTeam organization invite", body)
}

func (mailer *sesMailer) sendPlainTextEmail(ctx context.Context, email string, subject string, body string) error {
	trimmedEmail := strings.TrimSpace(email)
	if trimmedEmail == "" {
		return fmt.Errorf("email is required")
	}
	charset := emailCharset
	_, err := mailer.client.SendEmail(ctx, &sesv2.SendEmailInput{
		FromEmailAddress: &mailer.fromAddress,
		Destination: &sestypes.Destination{
			ToAddresses: []string{trimmedEmail},
		},
		Content: &sestypes.EmailContent{
			Simple: &sestypes.Message{
				Subject: &sestypes.Content{Data: &subject, Charset: &charset},
				Body: &sestypes.Body{
					Text: &sestypes.Content{Data: &body, Charset: &charset},
				},
			},
		},
	})
	if err != nil {
		productionAccessEnabled := "unknown"
		if mailer.productionAccessEnabled != nil {
			productionAccessEnabled = strconv.FormatBool(*mailer.productionAccessEnabled)
		}
		return fmt.Errorf("send email via SES (productionAccessEnabled=%s): %w", productionAccessEnabled, err)
	}
	return nil
}

func TokenLink(publicBaseURL string, kind string, token string) string {
	queryKey := tokenQueryKey(kind)
	return queryLink(publicBaseURL, queryKey, token)
}

func PendingInviteLink(publicBaseURL string, inviteID string) string {
	return pathLink(publicBaseURL, "invite", inviteID)
}

func pathLink(publicBaseURL string, segments ...string) string {
	joined, err := url.JoinPath(publicBaseURL, segments...)
	if err != nil {
		return "/" + strings.Join(segments, "/")
	}
	return joined
}

func queryLink(publicBaseURL string, queryKey string, value string) string {
	base, err := url.Parse(publicBaseURL)
	if err != nil {
		return fmt.Sprintf("?%s=%s", queryKey, value)
	}
	if base.Path == "" {
		base.Path = "/"
	}
	query := base.Query()
	query.Set(queryKey, value)
	base.RawQuery = query.Encode()
	return base.String()
}

func tokenLink(publicBaseURL string, kind string, token string) string {
	return TokenLink(publicBaseURL, kind, token)
}

func tokenQueryKey(kind string) string {
	switch kind {
	case "verify-email":
		return "verify"
	case "reset-password":
		return "reset"
	case "accept-invite":
		return "invite"
	default:
		return "token"
	}
}
