// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package event

import (
	"errors"
	"fmt"
	"os"
	"strconv"
	"strings"
	"time"

	"github.com/hashicorp/go-secure-stdlib/parseutil"

	"github.com/hashicorp/go-uuid"
)

// Option is how Options are passed as arguments.
type Option func(*options) error

// Options are used to represent configuration for an Event.
type options struct {
	withID          string
	withNow         time.Time
	withFacility    string
	withTag         string
	withSocketType  string
	withMaxDuration time.Duration
	withFileMode    *os.FileMode
}

// getDefaultOptions returns Options with their default values.
func getDefaultOptions() options {
	fileMode := os.FileMode(0o600)

	return options{
		withNow:         time.Now(),
		withFacility:    "AUTH",
		withTag:         "vault",
		withSocketType:  "tcp",
		withMaxDuration: 2 * time.Second,
		withFileMode:    &fileMode,
	}
}

// getOpts applies all the supplied Option and returns configured Options.
// Each Option is applied in the order it appears in the argument list, so it is
// possible to supply the same Option numerous times and the 'last write wins'.
func getOpts(opt ...Option) (options, error) {
	opts := getDefaultOptions()
	for _, o := range opt {
		if o == nil {
			continue
		}
		if err := o(&opts); err != nil {
			return options{}, err
		}
	}
	return opts, nil
}

// NewID is a bit of a modified NewID has been done to stop a circular
// dependency with the errors package that is caused by importing
// boundary/internal/db
func NewID(prefix string) (string, error) {
	const op = "event.NewID"
	if prefix == "" {
		return "", fmt.Errorf("%s: missing prefix: %w", op, ErrInvalidParameter)
	}

	id, err := uuid.GenerateUUID()
	if err != nil {
		return "", fmt.Errorf("%s: unable to generate ID: %w", op, err)
	}

	return fmt.Sprintf("%s_%s", prefix, id), nil
}

// WithID provides an optional ID.
func WithID(id string) Option {
	return func(o *options) error {
		var err error

		id := strings.TrimSpace(id)
		switch {
		case id == "":
			err = errors.New("id cannot be empty")
		default:
			o.withID = id
		}

		return err
	}
}

// WithNow provides an option to represent 'now'.
func WithNow(now time.Time) Option {
	return func(o *options) error {
		var err error

		switch {
		case now.IsZero():
			err = errors.New("cannot specify 'now' to be the zero time instant")
		default:
			o.withNow = now
		}

		return err
	}
}

// WithFacility provides an Option to represent a 'facility' for a syslog sink.
func WithFacility(facility string) Option {
	return func(o *options) error {
		o.withFacility = facility

		return nil
	}
}

// WithTag provides an Option to represent a 'tag' for a syslog sink.
func WithTag(tag string) Option {
	return func(o *options) error {
		o.withTag = tag

		return nil
	}
}

// WithSocketType provides an Option to represent the socket type for a socket sink.
func WithSocketType(socketType string) Option {
	return func(o *options) error {
		socketType = strings.TrimSpace(socketType)

		if socketType != "" {
			o.withSocketType = socketType
		}

		return nil
	}
}

// WithMaxDuration provides an Option to represent the max duration for writing to a socket.
func WithMaxDuration(duration string) Option {
	return func(o *options) error {
		duration = strings.TrimSpace(duration)

		if duration == "" {
			return nil
		}

		parsed, err := parseutil.ParseDurationSecond(duration)
		if err != nil {
			return err
		}

		o.withMaxDuration = parsed

		return nil
	}
}

// WithFileMode provides an Option to represent a file mode for a file sink.
// Supplying an empty string or whitespace will prevent this Option from being
// applied, but it will not return an error in those circumstances.
func WithFileMode(mode string) Option {
	return func(o *options) error {
		// If supplied file mode is empty, just return early without setting anything.
		// We can assume that this Option was called by something that didn't
		// parse the incoming value, perhaps from a config map etc.
		mode = strings.TrimSpace(mode)
		if mode == "" {
			return nil
		}

		// By now we believe we have something that the caller really intended to
		// be parsed into a file mode.
		raw, err := strconv.ParseUint(mode, 8, 32)

		switch {
		case err != nil:
			return fmt.Errorf("unable to parse file mode: %w", err)
		default:
			m := os.FileMode(raw)
			o.withFileMode = &m
		}

		return nil
	}
}
