package email

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"log/slog"
	"net/http"
	"strings"
	"time"
)

const (
	defaultPostmarkURL = "https://api.postmarkapp.com"
	postmarkTimeout    = 15 * time.Second
)

// PostmarkSender sends email via the Postmark API.
type PostmarkSender struct {
	serverToken string
	apiURL      string
	from        string
	client      *http.Client
	logger      *slog.Logger
}

// NewPostmarkSender constructs a Postmark sender. apiURL may be empty to use
// the public Postmark endpoint; tests can point it at an httptest server.
func NewPostmarkSender(serverToken, from, apiURL string, logger *slog.Logger) *PostmarkSender {
	if apiURL == "" {
		apiURL = defaultPostmarkURL
	}
	return &PostmarkSender{
		serverToken: serverToken,
		apiURL:      strings.TrimRight(apiURL, "/"),
		from:        from,
		client: &http.Client{
			Timeout: postmarkTimeout,
			CheckRedirect: func(req *http.Request, via []*http.Request) error {
				return http.ErrUseLastResponse
			},
		},
		logger: logger,
	}
}

type postmarkRequest struct {
	From     string           `json:"From"`
	To       string           `json:"To"`
	Subject  string           `json:"Subject"`
	TextBody string           `json:"TextBody"`
	Headers  []postmarkHeader `json:"Headers,omitempty"`
}

type postmarkHeader struct {
	Name  string `json:"Name"`
	Value string `json:"Value"`
}

type postmarkResponse struct {
	ErrorCode int    `json:"ErrorCode"`
	Message   string `json:"Message"`
	To        string `json:"To"`
}

// WithReplyTo sets a Reply-To header on the outgoing message. Returns a copy
// of the sender with the reply-to address applied for the next Send call.
// Not safe for concurrent use; callers should construct a fresh sender per
// send if they need different reply-to addresses.
func (p *PostmarkSender) Send(ctx context.Context, to []string, subject, body string) error {
	if p.serverToken == "" {
		return &PermanentError{Cause: fmt.Errorf("postmark server token not configured")}
	}
	if p.from == "" {
		return &PermanentError{Cause: fmt.Errorf("postmark from address not configured")}
	}
	if len(to) == 0 {
		return &PermanentError{Cause: fmt.Errorf("postmark send requires at least one recipient")}
	}

	payload := postmarkRequest{
		From:     p.from,
		To:       strings.Join(to, ","),
		Subject:  subject,
		TextBody: body,
	}
	bodyBytes, err := json.Marshal(payload)
	if err != nil {
		return fmt.Errorf("marshal postmark request: %w", err)
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodPost, p.apiURL+"/email", bytes.NewReader(bodyBytes))
	if err != nil {
		return fmt.Errorf("build postmark request: %w", err)
	}
	req.Header.Set("Accept", "application/json")
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("X-Postmark-Server-Token", p.serverToken)

	resp, err := p.client.Do(req)
	if err != nil {
		return &TemporaryError{Cause: fmt.Errorf("postmark request: %w", err)}
	}
	defer resp.Body.Close()

	respBytes, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<14))

	if resp.StatusCode >= 500 {
		return &TemporaryError{Cause: fmt.Errorf("postmark server error: status=%d body=%s", resp.StatusCode, truncate(string(respBytes)))}
	}

	if resp.StatusCode != http.StatusOK {
		return &PermanentError{Cause: fmt.Errorf("postmark rejected: status=%d body=%s", resp.StatusCode, truncate(string(respBytes)))}
	}

	var parsed postmarkResponse
	if err := json.Unmarshal(respBytes, &parsed); err != nil {
		return fmt.Errorf("decode postmark response: %w", err)
	}
	if parsed.ErrorCode != 0 {
		// Postmark distinguishes "inactive recipient" (406) and other errors. Treat
		// 406 as non-retryable; others bubble up as generic failures.
		return &PermanentError{Cause: fmt.Errorf("postmark error code=%d: %s", parsed.ErrorCode, parsed.Message)}
	}

	p.logger.Info("postmark email sent", "to", parsed.To, "subject", subject)
	return nil
}

func truncate(s string) string {
	if len(s) > 256 {
		return s[:256] + "..."
	}
	return s
}

// TemporaryError wraps a failure that callers may retry. The queue worker
// uses errors.As to detect it and apply exponential backoff.
type TemporaryError struct {
	Cause error
}

func (t *TemporaryError) Error() string { return t.Cause.Error() }
func (t *TemporaryError) Unwrap() error { return t.Cause }

// PermanentError marks a provider rejection that cannot succeed on retry.
type PermanentError struct {
	Cause error
}

func (p *PermanentError) Error() string { return p.Cause.Error() }
func (p *PermanentError) Unwrap() error { return p.Cause }
