// Copyright (c) 2024 Shivaram Lingamneni <slingamn@cs.stanford.edu>
// Released under the MIT license
// Some portions of this code are:
// Copyright (c) 2021-2024 Simon Ser <contact@emersion.fr>
// Originally released under the AGPLv3, relicensed to the Ergo project under the MIT license

package webpush

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"time"

	"github.com/ergochat/irc-go/ircmsg"
	webpush "github.com/ergochat/webpush-go/v2"

	"github.com/ergochat/ergo/irc/utils"
)

// alias some public types and names from webpush-go
type VAPIDKeys = webpush.VAPIDKeys
type Keys = webpush.Keys

var (
	GenerateVAPIDKeys = webpush.GenerateVAPIDKeys
)

// Urgency is a uint8 representation of urgency to save a few
// bytes on channel sizes.
type Urgency uint8

const (
	// UrgencyVeryLow requires device state: on power and Wi-Fi
	UrgencyVeryLow Urgency = iota // "very-low"
	// UrgencyLow requires device state: on either power or Wi-Fi
	UrgencyLow // "low"
	// UrgencyNormal excludes device state: low battery
	UrgencyNormal // "normal"
	// UrgencyHigh admits device state: low battery
	UrgencyHigh // "high"
)

var (
	// PingMessage is a valid IRC message that we can send to test that the subscription
	// is valid (i.e. responds to POSTs with a 20x). We do not expect that the client will
	// actually connect to IRC and send PONG (although it might be nice to have a way to
	// hint to a client that they should reconnect to renew their subscription?)
	PingMessage = []byte("PING webpush")
)

func convertUrgency(u Urgency) webpush.Urgency {
	switch u {
	case UrgencyVeryLow:
		return webpush.UrgencyVeryLow
	case UrgencyLow:
		return webpush.UrgencyLow
	case UrgencyNormal:
		return webpush.UrgencyNormal
	case UrgencyHigh:
		return webpush.UrgencyHigh
	default:
		return webpush.UrgencyNormal // shouldn't happen
	}
}

var httpClient webpush.HTTPClient = makeExternalOnlyClient()

var (
	Err404 = errors.New("endpoint returned a 404, indicating that the push subscription is no longer valid")

	errInvalidKey = errors.New("invalid key format")
)

func DecodeSubscriptionKeys(keysParam string) (keys webpush.Keys, err error) {
	// The keys parameter is tag-encoded, with each tag value being URL-safe base64 encoded:
	// * One public key with the name p256dh set to the client's P-256 ECDH public key.
	// * One shared key with the name auth set to a 16-byte client-generated authentication secret.
	// since we don't have a separate tag parser implementation, wrap it in a fake IRC line for parsing:
	fakeIRCLine := fmt.Sprintf("@%s PING", keysParam)
	ircMsg, err := ircmsg.ParseLine(fakeIRCLine)
	if err != nil {
		return
	}
	_, auth := ircMsg.GetTag("auth")
	_, p256 := ircMsg.GetTag("p256dh")
	return webpush.DecodeSubscriptionKeys(auth, p256)
}

// MakePushMessage serializes a utils.SplitMessage as a web push message (the args are in
// logical order)
func MakePushMessage(command, nuh, accountName, target string, msg utils.SplitMessage) ([]byte, error) {
	var messageForPush string
	if msg.Is512() {
		messageForPush = msg.Message
	} else {
		messageForPush = msg.Split[0].Message
	}
	return MakePushLine(msg.Time, accountName, nuh, command, target, messageForPush)
}

// MakePushLine serializes an arbitrary IRC line as a web push message (the args are in
// IRC syntax order)
func MakePushLine(time time.Time, accountName, source, command string, params ...string) ([]byte, error) {
	pushMessage := ircmsg.MakeMessage(nil, source, command, params...)
	pushMessage.SetTag("time", time.Format(utils.IRCv3TimestampFormat))
	// "*" is canonical for the unset form of the unfolded account name, but check both:
	if accountName != "*" && accountName != "" {
		pushMessage.SetTag("account", accountName)
	}
	if line, err := pushMessage.LineBytesStrict(false, 512); err == nil {
		// strip final \r\n
		return line[:len(line)-2], nil
	} else {
		return nil, err
	}
}

func SendWebPush(ctx context.Context, endpoint string, keys Keys, vapidKeys *VAPIDKeys, urgency Urgency, subscriber string, msg []byte) error {
	wpsub := webpush.Subscription{
		Endpoint: endpoint,
		Keys:     keys,
	}

	options := webpush.Options{
		HTTPClient: httpClient,
		VAPIDKeys:  vapidKeys,
		Subscriber: subscriber,
		TTL:        7 * 24 * 60 * 60, // seconds
		Urgency:    convertUrgency(urgency),
		RecordSize: 2048,
	}

	resp, err := webpush.SendNotification(ctx, msg, &wpsub, &options)
	if err != nil {
		return err
	}
	resp.Body.Close()

	if resp.StatusCode == http.StatusNotFound {
		return Err404
	} else if 200 <= resp.StatusCode && resp.StatusCode < 300 {
		return nil
	} else {
		return fmt.Errorf("HTTP error: %v", resp.Status)
	}
}
