package notification

import (
	"context"
	"time"

	"galaxy/backend/internal/config"
	"galaxy/backend/internal/user"
	"galaxy/backend/push"

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

// PushPublisher is the publisher contract notification uses to emit a
// `client_event` push frame to gateway. The real implementation lives
// in `backend/push` (`*push.Service`); NewNoopPushPublisher satisfies
// the interface for tests that do not exercise push behaviour.
//
// `event` is a typed `push.Event`: the publisher invokes Marshal on
// the event at publish time, so producers stay decoupled from the
// wire encoding. Every catalog kind has a FlatBuffers schema in
// `pkg/schema/fbs/notification.fbs` and is built by
// `buildClientPushEvent`; an unknown kind falls back to
// `push.JSONEvent` so a misconfigured producer keeps the pipeline
// flowing.
//
// Implementations must be concurrency-safe. The deviceSessionID pointer
// narrows the event to a single device session when non-nil; nil means
// fan out to every active session of userID. eventID, requestID and
// traceID are correlation identifiers that gateway forwards verbatim
// into the signed client envelope; empty strings are forwarded
// unchanged.
type PushPublisher interface {
	PublishClientEvent(ctx context.Context, userID uuid.UUID, deviceSessionID *uuid.UUID, event push.Event, eventID, requestID, traceID string) error
}

// Mailer is the email surface notification uses for outbound mail. The
// canonical implementation is `*mail.Service.EnqueueTemplate`; tests
// substitute a recording fake. The contract matches mail's existing
// signature so the wiring layer can pass the concrete service directly.
type Mailer interface {
	EnqueueTemplate(ctx context.Context, templateID, recipient string, payload map[string]any, idempotencyKey string) error
}

// AccountResolver looks up the recipient profile (email + preferred
// language) by user_id. The canonical implementation is
// `*user.Service.GetAccount`. The narrow interface keeps the Service
// from depending on every part of the user surface.
type AccountResolver interface {
	GetAccount(ctx context.Context, userID uuid.UUID) (user.Account, error)
}

// Deps aggregates every collaborator the Service depends on.
//
// Store, Mail, and Accounts must be non-nil. Push defaults to the no-op
// publisher when omitted; Now defaults to time.Now; Logger defaults to
// zap.NewNop. Config carries the worker interval, the max-attempts cap,
// and the optional admin-email destination from `BACKEND_NOTIFICATION_*`.
type Deps struct {
	Store *Store
	Mail Mailer
	Push PushPublisher
	Accounts AccountResolver
	Config config.NotificationConfig
	// Now overrides time.Now for deterministic tests. A nil Now defaults
	// to time.Now in NewService.
	Now func() time.Time
	// Logger is named under "notification" by NewService. Nil falls back
	// to zap.NewNop.
	Logger *zap.Logger
}

// NewNoopPushPublisher returns a PushPublisher that logs every event
// at debug level and returns nil. The canonical publisher lives in
// `backend/internal/push`; this constructor exists for tests.
func NewNoopPushPublisher(logger *zap.Logger) PushPublisher {
	if logger == nil {
		logger = zap.NewNop()
	}
	return &noopPushPublisher{logger: logger.Named("push.noop")}
}

type noopPushPublisher struct {
	logger *zap.Logger
}

func (p *noopPushPublisher) PublishClientEvent(_ context.Context, userID uuid.UUID, deviceSessionID *uuid.UUID, event push.Event, eventID, requestID, traceID string) error {
	kind := ""
	if event != nil {
		kind = event.Kind()
	}
	fields := []zap.Field{
		zap.String("user_id", userID.String()),
		zap.String("kind", kind),
	}
	if deviceSessionID != nil {
		fields = append(fields, zap.String("device_session_id", deviceSessionID.String()))
	}
	if eventID != "" {
		fields = append(fields, zap.String("event_id", eventID))
	}
	if requestID != "" {
		fields = append(fields, zap.String("request_id", requestID))
	}
	if traceID != "" {
		fields = append(fields, zap.String("trace_id", traceID))
	}
	p.logger.Debug("client event (noop publisher)", fields...)
	return nil
}
