// Package push delivers mobile push notifications to registered device tokens
// via FCM (Android, HTTP v1) and APNS (iOS, HTTP/2). When credentials are not
// configured the dispatcher degrades to a no-op so local development keeps
// working without Firebase/Apple secrets.
package push

import (
	"context"
	"errors"
	"log/slog"

	"github.com/jackc/pgx/v5/pgxpool"
)

// Config holds FCM/APNS credentials. All fields are optional; a provider is only
// enabled when its required fields are present.
type Config struct {
	// FCM (Android) — service account JSON (HTTP v1 API).
	FCMServiceAccountJSON string
	FCMProjectID          string // optional; derived from the service account JSON when empty

	// APNS (iOS) — token-based auth (.p8 key).
	APNSPrivateKey string // PEM contents of the AuthKey_XXXX.p8 file
	APNSKeyID      string
	APNSTeamID     string
	APNSBundleID   string // apns-topic (app bundle identifier)
	APNSProduction bool   // false -> sandbox gateway
}

// Notification is the platform-agnostic payload fanned out to devices.
type Notification struct {
	Title string
	Body  string
	// Data is delivered as the FCM data map / APNS custom keys. Values must be strings.
	Data map[string]string
}

// deviceToken is a single registered device row.
type deviceToken struct {
	id        string
	platform  string
	app       string
	pushToken string
}

// Dispatcher loads device tokens and sends notifications best-effort.
type Dispatcher struct {
	db     *pgxpool.Pool
	fcm    *fcmClient
	apns   *apnsClient
	logger *slog.Logger
}

// NewDispatcher builds a dispatcher. Missing credentials disable the relevant
// provider; an entirely unconfigured dispatcher silently skips all sends.
func NewDispatcher(db *pgxpool.Pool, cfg Config) *Dispatcher {
	logger := slog.Default().With("component", "push")

	d := &Dispatcher{db: db, logger: logger}

	if fcm, err := newFCMClient(cfg); err != nil {
		logger.Warn("fcm push disabled", "error", err)
	} else if fcm != nil {
		d.fcm = fcm
		logger.Info("fcm push enabled", "project_id", fcm.projectID)
	}

	if apns, err := newAPNSClient(cfg); err != nil {
		logger.Warn("apns push disabled", "error", err)
	} else if apns != nil {
		d.apns = apns
		logger.Info("apns push enabled", "bundle_id", apns.topic, "production", cfg.APNSProduction)
	}

	return d
}

// Enabled reports whether at least one provider is configured.
func (d *Dispatcher) Enabled() bool {
	return d != nil && (d.fcm != nil || d.apns != nil)
}

// NotifyUser fans a notification out to every registered device of the given
// internal user id (users.id UUID). It is best-effort: failures are logged and
// stale tokens are pruned, but no error is returned.
func (d *Dispatcher) NotifyUser(ctx context.Context, userID string, n Notification) {
	if d == nil || d.db == nil || userID == "" {
		return
	}
	if !d.Enabled() {
		return
	}

	tokens, err := d.loadTokens(ctx, userID)
	if err != nil {
		d.logger.Error("load device tokens", "user_id", userID, "error", err)
		return
	}
	if len(tokens) == 0 {
		return
	}

	for _, t := range tokens {
		var sendErr error
		switch t.platform {
		case "android":
			if d.fcm == nil {
				continue
			}
			sendErr = d.fcm.send(ctx, t.pushToken, n)
		case "ios":
			if d.apns == nil {
				continue
			}
			sendErr = d.apns.send(ctx, t.pushToken, n)
		default:
			continue
		}

		if sendErr == nil {
			continue
		}
		if errors.Is(sendErr, errTokenUnregistered) {
			d.deleteToken(ctx, t.id)
			d.logger.Info("pruned unregistered device token", "platform", t.platform, "app", t.app)
			continue
		}
		d.logger.Warn("push send failed", "platform", t.platform, "app", t.app, "error", sendErr)
	}
}

// NotifyNewMail sends a "new mail" notification to the user's devices. It
// satisfies the imap.Pusher interface used by the mail sync pipeline.
func (d *Dispatcher) NotifyNewMail(ctx context.Context, userID, messageID, accountID, sender, subject string) {
	if d == nil || !d.Enabled() {
		return
	}
	title := sender
	if title == "" {
		title = "New mail"
	}
	d.NotifyUser(ctx, userID, Notification{
		Title: title,
		Body:  subject,
		Data: map[string]string{
			"type":       "mail.created",
			"message_id": messageID,
			"account_id": accountID,
		},
	})
}

func (d *Dispatcher) loadTokens(ctx context.Context, userID string) ([]deviceToken, error) {
	rows, err := d.db.Query(ctx, `
		SELECT id::text, platform, app, push_token
		FROM device_tokens
		WHERE user_id = $1
	`, userID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var out []deviceToken
	for rows.Next() {
		var t deviceToken
		if err := rows.Scan(&t.id, &t.platform, &t.app, &t.pushToken); err != nil {
			return nil, err
		}
		out = append(out, t)
	}
	return out, rows.Err()
}

func (d *Dispatcher) deleteToken(ctx context.Context, id string) {
	if _, err := d.db.Exec(ctx, `DELETE FROM device_tokens WHERE id = $1`, id); err != nil {
		d.logger.Warn("delete stale device token", "id", id, "error", err)
	}
}

// errTokenUnregistered signals that a device token is no longer valid and
// should be removed from storage.
var errTokenUnregistered = errors.New("push: device token unregistered")
