package mail

import (
	"context"
	"fmt"
	netmail "net/mail"
	"strings"
	"time"

	"github.com/google/uuid"
	"go.uber.org/zap"
)

// contentTypeTextPlain is the RFC 2046 text/plain MIME type stored in
// `mail_payloads.content_type` for plain-text bodies.
const contentTypeTextPlain = "text/plain"

// TemplateLoginCode is the template_id stored in `mail_deliveries` for
// the auth-issued login code. The value matches the kind in the
// notification catalog (`README.md` §10) so future cross-reporting
// stays consistent.
const TemplateLoginCode = "auth.login_code"

// EnqueueLoginCode renders the auth login-code email and inserts the
// outbox row. Each call gets a fresh server-side idempotency_key so
// the unique constraint cannot accidentally suppress a legitimate
// re-issue; double-enqueue protection lives in the auth challenge
// throttle (see `auth.Service.SendEmailCode`).
func (s *Service) EnqueueLoginCode(ctx context.Context, email, code string, ttl time.Duration) error {
	addr, err := normaliseRecipient(email)
	if err != nil {
		return err
	}
	subject, body := renderLoginCode(code, ttl)
	args := EnqueueArgs{
		DeliveryID: uuid.New(),
		TemplateID: TemplateLoginCode,
		IdempotencyKey: uuid.NewString(),
		Recipients: []string{addr},
		ContentType: contentTypeTextPlain,
		Subject: subject,
		Body: []byte(body),
	}
	inserted, err := s.deps.Store.InsertEnqueue(ctx, args)
	if err != nil {
		return fmt.Errorf("mail: enqueue login code: %w", err)
	}
	if !inserted {
		// Cannot happen given the random key, but keeps the invariant
		// explicit for readers grep-ing for unexpected paths.
		s.deps.Logger.Warn("login-code enqueue collided on random idempotency key",
			zap.String("delivery_id", args.DeliveryID.String()))
	}
	return nil
}

// EnqueueTemplate is the generic producer surface used by future
// notification fan-out . Caller supplies a stable
// idempotencyKey so re-deliveries of the same logical event are
// deduplicated by the (template_id, idempotency_key) UNIQUE
// constraint.
func (s *Service) EnqueueTemplate(ctx context.Context, templateID, recipient string, payload map[string]any, idempotencyKey string) error {
	if strings.TrimSpace(idempotencyKey) == "" {
		return fmt.Errorf("mail: idempotency_key must not be empty")
	}
	addr, err := normaliseRecipient(recipient)
	if err != nil {
		return err
	}
	render, ok := templateRenderers[templateID]
	if !ok {
		return fmt.Errorf("%w: %q", ErrUnknownTemplate, templateID)
	}
	subject, body, err := render(payload)
	if err != nil {
		return fmt.Errorf("mail: render template %q: %w", templateID, err)
	}
	args := EnqueueArgs{
		DeliveryID: uuid.New(),
		TemplateID: templateID,
		IdempotencyKey: idempotencyKey,
		Recipients: []string{addr},
		ContentType: contentTypeTextPlain,
		Subject: subject,
		Body: []byte(body),
	}
	if _, err := s.deps.Store.InsertEnqueue(ctx, args); err != nil {
		return fmt.Errorf("mail: enqueue template: %w", err)
	}
	return nil
}

// normaliseRecipient trims whitespace and validates the address with
// stdlib RFC 5322 parsing. Empty / malformed addresses are rejected
// with ErrInvalidRecipient. The returned string is the canonical form
// (`mail.Address.Address`) without any display name.
func normaliseRecipient(addr string) (string, error) {
	trimmed := strings.TrimSpace(addr)
	if trimmed == "" {
		return "", ErrInvalidRecipient
	}
	parsed, err := netmail.ParseAddress(trimmed)
	if err != nil {
		return "", ErrInvalidRecipient
	}
	return parsed.Address, nil
}

// templateRenderers is the inline catalog of mail templates the
// notification module dispatches against. The implementation added
// `auth.login_code`; The implementation added the rest of the email-bearing
// kinds enumerated in `README.md` §10. Each renderer takes the
// producer-supplied payload map and returns (subject, body) or an
// error when required fields are missing or wrongly typed.
var templateRenderers = map[string]func(map[string]any) (string, string, error){
	TemplateLoginCode: func(payload map[string]any) (string, string, error) {
		code, _ := payload["code"].(string)
		if code == "" {
			return "", "", fmt.Errorf("payload.code must be a non-empty string")
		}
		ttl, _ := payload["ttl"].(time.Duration)
		subject, body := renderLoginCode(code, ttl)
		return subject, body, nil
	},
	"lobby.invite.received": func(payload map[string]any) (string, string, error) {
		gameID := payloadString(payload, "game_id")
		inviter := payloadString(payload, "inviter_user_id")
		subject := "You have a new Galaxy game invite"
		body := fmt.Sprintf(
			"You have been invited to a Galaxy game.\n\nGame: %s\nInviter: %s\n\nOpen the Galaxy client to accept or decline.\n",
			gameID, inviter,
		)
		return subject, body, nil
	},
	"lobby.application.approved": func(payload map[string]any) (string, string, error) {
		gameID := payloadString(payload, "game_id")
		subject := "Your Galaxy application was approved"
		body := fmt.Sprintf(
			"Your application to join the Galaxy game %s has been approved. The game owner will start the match when ready.\n",
			gameID,
		)
		return subject, body, nil
	},
	"lobby.application.rejected": func(payload map[string]any) (string, string, error) {
		gameID := payloadString(payload, "game_id")
		subject := "Your Galaxy application was rejected"
		body := fmt.Sprintf(
			"Your application to join the Galaxy game %s has been rejected. You can apply to other public games from the lobby.\n",
			gameID,
		)
		return subject, body, nil
	},
	"lobby.membership.removed": func(payload map[string]any) (string, string, error) {
		gameID := payloadString(payload, "game_id")
		reason := payloadString(payload, "reason")
		subject := "You were removed from a Galaxy game"
		body := fmt.Sprintf(
			"Your membership in the Galaxy game %s has been removed.\n\nReason: %s\n",
			gameID, fallbackString(reason, "no reason provided"),
		)
		return subject, body, nil
	},
	"lobby.membership.blocked": func(payload map[string]any) (string, string, error) {
		gameID := payloadString(payload, "game_id")
		subject := "You were blocked from a Galaxy game"
		body := fmt.Sprintf(
			"Your membership in the Galaxy game %s has been blocked. Please contact the game owner if this is unexpected.\n",
			gameID,
		)
		return subject, body, nil
	},
	"lobby.race_name.pending": func(payload map[string]any) (string, string, error) {
		raceName := payloadString(payload, "race_name")
		expiresAt := payloadString(payload, "expires_at")
		subject := "Your Galaxy race name is awaiting registration"
		body := fmt.Sprintf(
			"Congratulations — your Galaxy race name %q has reached pending registration. Confirm registration before %s to lock it permanently.\n",
			raceName, fallbackString(expiresAt, "the listed deadline"),
		)
		return subject, body, nil
	},
	"runtime.image_pull_failed": func(payload map[string]any) (string, string, error) {
		gameID := payloadString(payload, "game_id")
		imageRef := payloadString(payload, "image_ref")
		subject := "Galaxy runtime: image pull failed"
		body := fmt.Sprintf(
			"Image pull failed while preparing engine container for game %s.\n\nimage_ref: %s\n\nReview the runtime operation log for details.\n",
			gameID, fallbackString(imageRef, "unknown"),
		)
		return subject, body, nil
	},
	"runtime.container_start_failed": func(payload map[string]any) (string, string, error) {
		gameID := payloadString(payload, "game_id")
		subject := "Galaxy runtime: container start failed"
		body := fmt.Sprintf(
			"Engine container start failed for game %s.\n\nReview the runtime operation log and Docker daemon logs for details.\n",
			gameID,
		)
		return subject, body, nil
	},
	"runtime.start_config_invalid": func(payload map[string]any) (string, string, error) {
		gameID := payloadString(payload, "game_id")
		reason := payloadString(payload, "reason")
		subject := "Galaxy runtime: start config invalid"
		body := fmt.Sprintf(
			"Engine container start was rejected by configuration validation for game %s.\n\nReason: %s\n",
			gameID, fallbackString(reason, "no reason provided"),
		)
		return subject, body, nil
	},
}

// payloadString fetches a string field from a notification payload
// without panicking on missing or wrongly-typed entries; an empty
// string is the documented fallback.
func payloadString(payload map[string]any, key string) string {
	v, _ := payload[key].(string)
	return v
}

// fallbackString returns alt when value is empty.
func fallbackString(value, alt string) string {
	if strings.TrimSpace(value) == "" {
		return alt
	}
	return value
}

// renderLoginCode builds the English plain-text body used for the
// `auth.login_code` template. Localisation is deferred to a future
// stage (see `backend/README.md` and `backend/docs/`).
func renderLoginCode(code string, ttl time.Duration) (subject, body string) {
	subject = fmt.Sprintf("Galaxy login code: %s", code)
	minutes := int(ttl.Round(time.Minute) / time.Minute)
	if minutes <= 0 {
		minutes = 1
	}
	body = fmt.Sprintf(
		"Your one-time Galaxy login code is %s.\n\nThe code expires in %d minutes. If you did not request it, you can ignore this email.\n",
		code, minutes,
	)
	return subject, body
}
