// Copyright IBM Corp. 2016, 2025
// SPDX-License-Identifier: BUSL-1.1

package command

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"os"
	"strings"

	"github.com/coder/websocket"
	"github.com/hashicorp/cli"
	"github.com/hashicorp/vault/api"
	"github.com/posener/complete"
)

var (
	_ cli.Command             = (*EventsSubscribeCommands)(nil)
	_ cli.CommandAutocomplete = (*EventsSubscribeCommands)(nil)
)

type EventsSubscribeCommands struct {
	*BaseCommand

	namespaces  []string
	bexprFilter string
}

func (c *EventsSubscribeCommands) Synopsis() string {
	return "Subscribe to events"
}

func (c *EventsSubscribeCommands) Help() string {
	helpText := `
Usage: vault events subscribe [-namespaces=ns1] [-timeout=XYZs] [-filter=filterExpression] eventType

  Subscribe to events of the given event type (topic), which may be a glob
  pattern (with "*" treated as a wildcard). The events will be sent to
  standard out.

  The output will be a JSON object serialized using the default protobuf
  JSON serialization format, with one line per event received.
` + c.Flags().Help()
	return strings.TrimSpace(helpText)
}

func (c *EventsSubscribeCommands) Flags() *FlagSets {
	set := c.flagSet(FlagSetHTTP)
	f := set.NewFlagSet("Subscribe Options")
	f.StringVar(&StringVar{
		Name: "filter",
		Usage: `A boolean expression to use to filter events. Only events matching
                the filter will be subscribed to. This is applied after any filtering
                by event type or namespace.`,
		Default: "",
		Target:  &c.bexprFilter,
	})
	f.StringSliceVar(&StringSliceVar{
		Name: "namespaces",
		Usage: `Specifies one or more patterns of additional child namespaces
                to subscribe to. The namespace of the request is automatically
                prepended, so specifying 'ns2' when the request is in the 'ns1'
                namespace will result in subscribing to 'ns1/ns2', in addition to
                'ns1'. Patterns can include "*" characters to indicate
                wildcards. The default is to subscribe only to the request's
                namespace.`,
		Default: []string{},
		Target:  &c.namespaces,
	})
	return set
}

func (c *EventsSubscribeCommands) AutocompleteArgs() complete.Predictor {
	return nil
}

func (c *EventsSubscribeCommands) AutocompleteFlags() complete.Flags {
	return c.Flags().Completions()
}

func (c *EventsSubscribeCommands) Run(args []string) int {
	f := c.Flags()

	if err := f.Parse(args); err != nil {
		c.UI.Error(err.Error())
		return 1
	}

	args = f.Args()
	switch {
	case len(args) < 1:
		c.UI.Error(fmt.Sprintf("Not enough arguments (expected 1, got %d)", len(args)))
		return 1
	case len(args) > 1:
		c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args)))
		return 1
	}

	client, err := c.Client()
	if err != nil {
		c.UI.Error(err.Error())
		return 2
	}

	err = c.subscribeRequest(client, "sys/events/subscribe/"+args[0])
	if err != nil {
		c.UI.Error(err.Error())
		return 1
	}
	return 0
}

// cleanNamespace removes leading and trailing space and /'s from the namespace path.
func cleanNamespace(ns string) string {
	ns = strings.TrimSpace(ns)
	ns = strings.Trim(ns, "/")
	return ns
}

// cleanNamespaces removes leading and trailing space and /'s from the namespace paths.
func cleanNamespaces(namespaces []string) []string {
	cleaned := make([]string, len(namespaces))
	for i, ns := range namespaces {
		cleaned[i] = cleanNamespace(ns)
	}
	return cleaned
}

func (c *EventsSubscribeCommands) subscribeRequest(client *api.Client, path string) error {
	r := client.NewRequest("GET", "/v1/"+path)
	u := r.URL
	if u.Scheme == "http" {
		u.Scheme = "ws"
	} else {
		u.Scheme = "wss"
	}
	q := u.Query()
	q.Set("json", "true")
	if len(c.namespaces) > 0 {
		q["namespaces"] = cleanNamespaces(c.namespaces)
	}
	bexprFilter := strings.TrimSpace(c.bexprFilter)
	if bexprFilter != "" {
		q.Set("filter", bexprFilter)
	}
	u.RawQuery = q.Encode()
	client.AddHeader("X-Vault-Token", client.Token())
	client.AddHeader("X-Vault-Namespace", client.Namespace())
	ctx := context.Background()

	// Follow redirects in case our request if our request is forwarded to the leader.
	url := u.String()
	var conn *websocket.Conn
	var err error
	for attempt := 0; attempt < 10; attempt++ {
		var resp *http.Response
		conn, resp, err = websocket.Dial(ctx, url, &websocket.DialOptions{
			HTTPClient: client.CloneConfig().HttpClient,
			HTTPHeader: client.Headers(),
		})

		if err == nil {
			break
		}

		switch {
		case resp == nil:
			return err
		case resp.StatusCode == http.StatusTemporaryRedirect:
			url = resp.Header.Get("Location")
			continue
		case resp.StatusCode == http.StatusNotFound:
			return errors.New("events endpoint not found; check `vault read sys/experiments` to see if an events experiment is available but disabled")
		default:
			return err
		}
	}

	if conn == nil {
		return fmt.Errorf("too many redirects")
	}
	defer conn.Close(websocket.StatusNormalClosure, "")

	for {
		_, message, err := conn.Read(ctx)
		if err != nil {
			return err
		}
		_, err = os.Stdout.Write(message)
		if err != nil {
			return err
		}
	}
}
