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

package audit

import (
	"fmt"
	"reflect"
	"strconv"
	"strings"

	"github.com/hashicorp/eventlogger"
	"github.com/hashicorp/vault/internal/observability/event"
)

const (
	stdout  = "stdout"
	discard = "discard"

	optionFilePath = "file_path"
	optionMode     = "mode"
)

var _ Backend = (*fileBackend)(nil)

type fileBackend struct {
	*backend
}

// NewFileBackend provides a wrapper to support the expectation elsewhere in Vault that
// all audit backends can be created via a factory that returns an interface (Backend).
func NewFileBackend(conf *BackendConfig, headersConfig HeaderFormatter) (be Backend, err error) {
	be, err = newFileBackend(conf, headersConfig)
	return
}

// newFileBackend creates a backend and configures all nodes including a file sink.
func newFileBackend(conf *BackendConfig, headersConfig HeaderFormatter) (*fileBackend, error) {
	if headersConfig == nil || reflect.ValueOf(headersConfig).IsNil() {
		return nil, fmt.Errorf("nil header formatter: %w", ErrInvalidParameter)
	}
	if conf == nil {
		return nil, fmt.Errorf("nil config: %w", ErrInvalidParameter)
	}
	if err := conf.Validate(); err != nil {
		return nil, err
	}

	// Get file path from config or fall back to the old option ('path') for compatibility
	// (see commit bac4fe0799a372ba1245db642f3f6cd1f1d02669).
	var filePath string
	if p, ok := conf.Config[optionFilePath]; ok {
		filePath = p
	} else if p, ok = conf.Config["path"]; ok {
		filePath = p
	} else {
		return nil, fmt.Errorf("%q is required: %w", optionFilePath, ErrExternalOptions)
	}

	bec, err := newBackend(headersConfig, conf)
	if err != nil {
		return nil, err
	}
	b := &fileBackend{backend: bec}

	// normalize file path if configured for stdout
	if strings.EqualFold(filePath, stdout) {
		filePath = stdout
	}
	if strings.EqualFold(filePath, discard) {
		filePath = discard
	}

	// Configure the sink.
	cfg, err := newFormatterConfig(headersConfig, conf.Config)
	if err != nil {
		return nil, err
	}

	sinkOpts := []event.Option{event.WithLogger(conf.Logger)}
	if mode, ok := conf.Config[optionMode]; ok {
		if strings.TrimSpace(mode) != "" {
			m, err := strconv.ParseUint(mode, 8, 32)
			if err != nil {
				return nil, fmt.Errorf("invalid mode: %s", mode)
			}
			if m&0o111 != 0 {
				return nil, fmt.Errorf("file mode may not be executable: %s", mode)
			}
		}
		sinkOpts = append(sinkOpts, event.WithFileMode(mode))
	}

	err = b.configureSinkNode(conf.MountPath, filePath, cfg.requiredFormat, sinkOpts...)
	if err != nil {
		return nil, err
	}

	return b, nil
}

// configureSinkNode is used internally by fileBackend to create and configure the
// sink node on the backend.
func (b *fileBackend) configureSinkNode(name string, filePath string, format format, opt ...event.Option) error {
	name = strings.TrimSpace(name)
	if name == "" {
		return fmt.Errorf("name is required: %w", ErrExternalOptions)
	}

	filePath = strings.TrimSpace(filePath)
	if filePath == "" {
		return fmt.Errorf("file path is required: %w", ErrExternalOptions)
	}

	sinkNodeID, err := event.GenerateNodeID()
	if err != nil {
		return fmt.Errorf("error generating random NodeID for sink node: %w: %w", ErrInternal, err)
	}

	// normalize file path if configured for stdout or discard
	if strings.EqualFold(filePath, stdout) {
		filePath = stdout
	} else if strings.EqualFold(filePath, discard) {
		filePath = discard
	}

	var sinkNode eventlogger.Node
	var sinkName string

	switch filePath {
	case stdout:
		sinkName = stdout
		sinkNode, err = event.NewStdoutSinkNode(format.String())
	case discard:
		sinkName = discard
		sinkNode = event.NewNoopSink()
	default:
		// The NewFileSink function attempts to open the file and will return an error if it can't.
		sinkName = name
		sinkNode, err = event.NewFileSink(filePath, format.String(), opt...)
	}
	if err != nil {
		return fmt.Errorf("file sink creation failed for path %q: %w", filePath, err)
	}

	// Wrap the sink node with metrics middleware
	err = b.wrapMetrics(sinkName, sinkNodeID, sinkNode)
	if err != nil {
		return err
	}

	return nil
}

// Reload will trigger the reload action on the sink node for this backend.
func (b *fileBackend) Reload() error {
	for _, n := range b.nodeMap {
		if n.Type() == eventlogger.NodeTypeSink {
			return n.Reopen()
		}
	}

	return nil
}
