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

package vault

import (
	"bytes"
	"context"
	"encoding/json"
	"net/http"
	"os"
	"strings"
	"sync"

	"github.com/hashicorp/vault/sdk/logical"
	"github.com/hashicorp/vault/sdk/physical"
)

const (
	uiConfigKey          = "config"
	uiConfigPlaintextKey = "config_plaintext"
)

// UIConfig contains UI configuration. This takes both a physical view and a barrier view
// because it is stored in both plaintext and encrypted to allow for getting the header
// values before the barrier is unsealed
type UIConfig struct {
	l               sync.RWMutex
	physicalStorage physical.Backend
	barrierStorage  logical.Storage

	enabled        bool
	defaultHeaders http.Header
}

// NewUIConfig creates a new UI config
func NewUIConfig(enabled bool, physicalStorage physical.Backend, barrierStorage logical.Storage) *UIConfig {
	defaultHeaders := http.Header{}
	connectSrcHeader := "connect-src 'self';"
	defaultHeaders.Set("Service-Worker-Allowed", "/")
	defaultHeaders.Set("X-Content-Type-Options", "nosniff")
	isHVD := os.Getenv("VAULT_CLOUD_ADMIN_NAMESPACE") // grab feature flag to determine if HVD
	if isHVD != "" && isHVD != "0" {                  // only if HVD, set connect-src to include posthog
		connectSrcHeader = "connect-src 'self' https://eu.i.posthog.com;"
	}
	defaultHeaders.Set("Content-Security-Policy", "default-src 'none'; "+connectSrcHeader+" img-src 'self' data:; script-src 'self'; style-src 'unsafe-inline' 'self'; form-action  'none'; frame-ancestors 'none'; font-src 'self'")
	return &UIConfig{
		physicalStorage: physicalStorage,
		barrierStorage:  barrierStorage,
		enabled:         enabled,
		defaultHeaders:  defaultHeaders,
	}
}

// Enabled returns if the UI is enabled
func (c *UIConfig) Enabled() bool {
	c.l.RLock()
	defer c.l.RUnlock()
	return c.enabled
}

// Headers returns the response headers that should be returned in the UI
func (c *UIConfig) Headers(ctx context.Context) (http.Header, error) {
	c.l.RLock()
	defer c.l.RUnlock()

	config, err := c.get(ctx)
	if err != nil {
		return nil, err
	}
	headers := make(http.Header)
	if config != nil {
		headers = config.Headers
	}

	for k := range c.defaultHeaders {
		if headers.Get(k) == "" {
			v := c.defaultHeaders.Get(k)
			headers.Set(k, v)
		}
	}
	return headers, nil
}

// HeaderKeys returns the list of the configured headers
func (c *UIConfig) HeaderKeys(ctx context.Context) ([]string, error) {
	c.l.RLock()
	defer c.l.RUnlock()

	config, err := c.get(ctx)
	if err != nil {
		return nil, err
	}
	if config == nil {
		return nil, nil
	}
	var keys []string
	for k := range config.Headers {
		keys = append(keys, k)
	}
	return keys, nil
}

// GetHeader retrieves the configured values for the given header
func (c *UIConfig) GetHeader(ctx context.Context, header string) ([]string, error) {
	c.l.RLock()
	defer c.l.RUnlock()

	config, err := c.get(ctx)
	if err != nil {
		return nil, err
	}
	if config == nil {
		return nil, nil
	}

	value := config.Headers.Values(header)
	return value, nil
}

// SetHeader sets the values for the given header
func (c *UIConfig) SetHeader(ctx context.Context, header string, values []string) error {
	c.l.Lock()
	defer c.l.Unlock()

	config, err := c.get(ctx)
	if err != nil {
		return err
	}
	if config == nil {
		config = &uiConfigEntry{
			Headers: http.Header{},
		}
	}

	// Clear custom header values before setting new
	config.Headers.Del(header)

	// Set new values
	for _, value := range values {
		config.Headers.Add(header, value)
	}
	return c.save(ctx, config)
}

// DeleteHeader deletes the header configuration for the given header
func (c *UIConfig) DeleteHeader(ctx context.Context, header string) error {
	c.l.Lock()
	defer c.l.Unlock()

	config, err := c.get(ctx)
	if err != nil {
		return err
	}
	if config == nil {
		return nil
	}

	config.Headers.Del(header)
	return c.save(ctx, config)
}

func (c *UIConfig) get(ctx context.Context) (*uiConfigEntry, error) {
	// Read plaintext always to ensure in sync with barrier value
	plaintextConfigRaw, err := c.physicalStorage.Get(ctx, uiConfigPlaintextKey)
	if err != nil {
		return nil, err
	}
	configRaw, uiConfigGetErr := c.barrierStorage.Get(ctx, uiConfigKey)

	// Respond with error only if not sealed, otherwise do not throw the error
	if uiConfigGetErr != nil && !strings.Contains(uiConfigGetErr.Error(), ErrBarrierSealed.Error()) {
		return nil, uiConfigGetErr
	}
	if configRaw == nil {
		return nil, nil
	}

	config := new(uiConfigEntry)
	if config == nil {
		return nil, nil
	}
	if err := json.Unmarshal(configRaw.Value, config); err != nil {
		return nil, err
	}

	// Check that plaintext value matches barrier value, if not sync values
	if uiConfigGetErr == nil && (plaintextConfigRaw == nil ||
		!bytes.Equal(plaintextConfigRaw.Value, configRaw.Value)) {
		if err := c.save(ctx, config); err != nil {
			return nil, err
		}
	}
	return config, nil
}

func (c *UIConfig) save(ctx context.Context, config *uiConfigEntry) error {
	if len(config.Headers) == 0 {
		if err := c.physicalStorage.Delete(ctx, uiConfigPlaintextKey); err != nil {
			return err
		}
		return c.barrierStorage.Delete(ctx, uiConfigKey)
	}

	configRaw, err := json.Marshal(config)
	if err != nil {
		return err
	}

	entry := &physical.Entry{
		Key:   uiConfigPlaintextKey,
		Value: configRaw,
	}
	if err := c.physicalStorage.Put(ctx, entry); err != nil {
		return err
	}

	barrEntry := &logical.StorageEntry{
		Key:   uiConfigKey,
		Value: configRaw,
	}
	return c.barrierStorage.Put(ctx, barrEntry)
}

type uiConfigEntry struct {
	Headers http.Header `json:"headers"`
}
