package notifications

import (
	"context"
	"database/sql"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"log"
	"net/http"
	"time"

	webpush "github.com/SherClockHolmes/webpush-go"
)

// WebPushChannel sends to every active push_subscriptions row. The VAPID
// keys come from the secrets store via VAPIDStore.
type WebPushChannel struct {
	DB         *sql.DB
	VAPID      *VAPIDStore
	HTTP       *http.Client
	Subject    string // operator-facing mailto: or https: URL per RFC 8292
	TTLSeconds int
}

func (c *WebPushChannel) Name() string { return "push" }

// Send pushes to every subscription. Per-subscription failures classified
// 410 Gone are auto-deleted (the browser dropped the subscription); other
// failures increment failure_count and surface in the per-row error_message.
func (c *WebPushChannel) Send(ctx context.Context, n Notification, _ map[string]any) (int, error) {
	if c.DB == nil || c.VAPID == nil {
		return 0, errors.New("webpush: not configured")
	}
	keys, err := c.VAPID.Load(ctx)
	if err != nil {
		return 0, err
	}
	subject := c.Subject
	if subject == "" {
		subject = "mailto:operator@example.com"
	}

	subs, err := c.loadSubscriptions(ctx)
	if err != nil {
		return 0, err
	}
	if len(subs) == 0 {
		return 0, ErrNoRecipients
	}

	payload, err := json.Marshal(map[string]any{
		"title":    n.Title,
		"body":     n.Body,
		"severity": string(n.Severity),
		"url":      n.URL,
		"data":     n.Data,
	})
	if err != nil {
		return 0, err
	}

	ttl := c.TTLSeconds
	if ttl <= 0 {
		ttl = 86400
	}
	hc := c.HTTP
	if hc == nil {
		hc = &http.Client{Timeout: 15 * time.Second}
	}

	delivered := 0
	for _, s := range subs {
		sub := &webpush.Subscription{
			Endpoint: s.Endpoint,
			Keys: webpush.Keys{
				P256dh: s.P256dh,
				Auth:   s.Auth,
			},
		}
		resp, err := webpush.SendNotificationWithContext(ctx, payload, sub, &webpush.Options{
			Subscriber:      subject,
			VAPIDPublicKey:  keys.Public,
			VAPIDPrivateKey: keys.Private,
			TTL:             ttl,
			HTTPClient:      hc,
		})
		if err != nil {
			c.bumpFailure(ctx, s.ID, err.Error())
			continue
		}
		body, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
		_ = resp.Body.Close()
		switch resp.StatusCode {
		case http.StatusGone, http.StatusNotFound:
			// Browser dropped the subscription; reclaim the row.
			_, _ = c.DB.ExecContext(ctx, `DELETE FROM push_subscriptions WHERE id = ?`, s.ID)
		case http.StatusCreated, http.StatusAccepted, http.StatusOK:
			delivered++
			_, _ = c.DB.ExecContext(ctx,
				`UPDATE push_subscriptions SET last_delivery_at = ?, failure_count = 0 WHERE id = ?`,
				time.Now().Unix(), s.ID)
		default:
			// Include the push service's response body — Apple/Mozilla put a
			// machine-readable reason there (e.g. {"reason":"BadJwtToken"}).
			c.bumpFailure(ctx, s.ID, fmt.Sprintf("%d %s: %s",
				resp.StatusCode, http.StatusText(resp.StatusCode), string(body)))
		}
	}
	if delivered == 0 {
		return 0, ErrNoRecipients
	}
	return delivered, nil
}

type pushSubRow struct {
	ID       int64
	Endpoint string
	P256dh   string
	Auth     string
}

func (c *WebPushChannel) loadSubscriptions(ctx context.Context) ([]pushSubRow, error) {
	rows, err := c.DB.QueryContext(ctx, `
		SELECT id, endpoint, p256dh, auth FROM push_subscriptions
		WHERE failure_count < 10
	`)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var out []pushSubRow
	for rows.Next() {
		var r pushSubRow
		if err := rows.Scan(&r.ID, &r.Endpoint, &r.P256dh, &r.Auth); err != nil {
			return nil, err
		}
		out = append(out, r)
	}
	return out, rows.Err()
}

func (c *WebPushChannel) bumpFailure(ctx context.Context, id int64, msg string) {
	_, _ = c.DB.ExecContext(ctx, `
		UPDATE push_subscriptions SET failure_count = failure_count + 1
		WHERE id = ?`, id)
	// Surface the real push-service error (Apple/Mozilla/FCM) instead of
	// silently dropping it — otherwise a failed send only shows up as the
	// dispatcher's generic "no recipients", which hides the actual cause.
	log.Printf("[webpush] send failed for subscription %d: %s", id, msg)
}
