package server

import (
	"context"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"os/signal"
	"sort"
	"strings"
	"sync"
	"syscall"
	"time"

	"github.com/hashicorp/errwrap"
	wrapping "github.com/openbao/go-kms-wrapping/v2"
	aeadwrapper "github.com/openbao/go-kms-wrapping/wrappers/aead/v2"
	"github.com/openbao/openbao/sdk/v2/logical"
	phy "github.com/openbao/openbao/sdk/v2/physical"
	physInmem "github.com/openbao/openbao/sdk/v2/physical/inmem"
	"github.com/spf13/cobra"
	wardenapi "github.com/stephnangue/warden/api"
	"github.com/stephnangue/warden/audit"
	"github.com/stephnangue/warden/auth/method/cert"
	"github.com/stephnangue/warden/auth/method/jwt"
	"github.com/stephnangue/warden/config"
	"github.com/stephnangue/warden/core"
	wardenseal "github.com/stephnangue/warden/core/seal"
	wardenhttp "github.com/stephnangue/warden/http"
	"github.com/stephnangue/warden/internal/configutil"
	"github.com/stephnangue/warden/listener"
	"github.com/stephnangue/warden/listener/api"
	clusterlistener "github.com/stephnangue/warden/listener/cluster"
	log "github.com/stephnangue/warden/logger"
	wardenlogical "github.com/stephnangue/warden/logical"
	"github.com/stephnangue/warden/physical"
	physPostgresql "github.com/stephnangue/warden/physical/postgresql"
	"github.com/stephnangue/warden/provider/alicloud"
	ansible_tower "github.com/stephnangue/warden/provider/ansible_tower"
	"github.com/stephnangue/warden/provider/anthropic"
	"github.com/stephnangue/warden/provider/atlassian"
	"github.com/stephnangue/warden/provider/aws"
	"github.com/stephnangue/warden/provider/azure"
	"github.com/stephnangue/warden/provider/cloudflare"
	"github.com/stephnangue/warden/provider/cohere"
	"github.com/stephnangue/warden/provider/datadog"
	"github.com/stephnangue/warden/provider/dynatrace"
	"github.com/stephnangue/warden/provider/elastic"
	"github.com/stephnangue/warden/provider/gcp"
	"github.com/stephnangue/warden/provider/github"
	"github.com/stephnangue/warden/provider/gitlab"
	"github.com/stephnangue/warden/provider/grafana"
	"github.com/stephnangue/warden/provider/honeycomb"
	"github.com/stephnangue/warden/provider/ibmcloud"
	"github.com/stephnangue/warden/provider/kubernetes"
	"github.com/stephnangue/warden/provider/mistral"
	"github.com/stephnangue/warden/provider/newrelic"
	"github.com/stephnangue/warden/provider/openai"
	"github.com/stephnangue/warden/provider/ovh"
	"github.com/stephnangue/warden/provider/pagerduty"
	"github.com/stephnangue/warden/provider/prometheus"
	"github.com/stephnangue/warden/provider/rds"
	"github.com/stephnangue/warden/provider/redshift"
	"github.com/stephnangue/warden/provider/scaleway"
	"github.com/stephnangue/warden/provider/sentry"
	"github.com/stephnangue/warden/provider/servicenow"
	"github.com/stephnangue/warden/provider/slack"
	"github.com/stephnangue/warden/provider/splunk"
	"github.com/stephnangue/warden/provider/tfe"
	"github.com/stephnangue/warden/provider/vault"
	"golang.org/x/text/cases"
	"golang.org/x/text/language"
)

const (
	// Subsystem names for logging
	subsystemCore     = "core"
	subsystemListener = "listener"

	// Listener type names
	listenerTypeTCP  = "tcp"
	listenerTypeUnix = "unix"
)

// buildVersion is the binary version surfaced in /v1/sys/health. Set by
// cmd/warden.SetVersion before the server starts so the value flows into
// http.HandlerProperties below.
var buildVersion string

// SetBuildVersion records the binary version for inclusion in the health
// response. Called from cmd.SetVersion.
func SetBuildVersion(v string) {
	buildVersion = v
}

var (
	configPath string
	configDir  string

	flagDev          bool
	flagDevRootToken string

	flagDevTLS                  bool
	flagDevTLSCertFile          string
	flagDevTLSKeyFile           string
	flagDevTLSCACertFile        string
	flagDevTLSRequireClientCert bool

	ServerCmd = &cobra.Command{
		Use:   "server",
		Short: "This command starts a Warden server that responds to API requests",
		Long: `
Usage: warden server [options]

  This command starts a Warden server that responds to API requests.

  Start a server with a single configuration file:

      $ warden server -config=/etc/warden/config.hcl

  Or merge multiple HCL files from a directory (lexical order, later files
  override earlier ones — useful for splitting a ConfigMap and a Secret in
  Kubernetes):

      $ warden server -config-dir=/etc/warden/conf.d

  Config files may reference environment variables via {{ env "VAR_NAME" }}.

  For a full list of examples, please see the documentation.
  `,
		RunE: run,
	}

	wg sync.WaitGroup

	cleanupGuard sync.Once

	auditDevices = map[string]audit.Factory{
		"file": &audit.FileDeviceFactory{},
	}

	providers = map[string]wardenlogical.Factory{
		"alicloud":      alicloud.Factory,
		"ansible_tower": ansible_tower.Factory,
		"anthropic":     anthropic.Factory,
		"atlassian":     atlassian.Factory,
		"aws":           aws.Factory,
		"azure":         azure.Factory,
		"cloudflare":    cloudflare.Factory,
		"cohere":        cohere.Factory,
		"datadog":       datadog.Factory,
		"dynatrace":     dynatrace.Factory,
		"elastic":       elastic.Factory,
		"kubernetes":    kubernetes.Factory,
		"gcp":           gcp.Factory,
		"grafana":       grafana.Factory,
		"honeycomb":     honeycomb.Factory,
		"ibmcloud":      ibmcloud.Factory,
		"github":        github.Factory,
		"gitlab":        gitlab.Factory,
		"mistral":       mistral.Factory,
		"newrelic":      newrelic.Factory,
		"openai":        openai.Factory,
		"ovh":           ovh.Factory,
		"pagerduty":     pagerduty.Factory,
		"prometheus":    prometheus.Factory,
		"servicenow":    servicenow.Factory,
		"slack":         slack.Factory,
		"splunk":        splunk.Factory,
		"tfe":           tfe.Factory,
		"vault":         vault.Factory,
		"rds":           rds.Factory,
		"redshift":      redshift.Factory,
		"scaleway":      scaleway.Factory,
		"sentry":        sentry.Factory,
	}

	// providerSkills maps provider type → agent-facing skill markdown,
	// shipped alongside each provider's Go code under provider/<type>/skill.md.
	// Only provider packages that ship a skill.md appear here; on first mount
	// of one of these types, the markdown is seeded into the global skill
	// registry. Operator edits to a seeded skill are preserved on subsequent
	// mounts (SkillStore.SeedProviderSkill is a no-op when the name exists).
	providerSkills = map[string]string{
		"ansible_tower": ansible_tower.Skill(),
		"atlassian":     atlassian.Skill(),
		"aws":           aws.Skill(),
		"github":        github.Skill(),
		"openai":        openai.Skill(),
		"rds":           rds.Skill(),
		"scaleway":      scaleway.Skill(),
		"slack":         slack.Skill(),
		"vault":         vault.Skill(),
	}

	authMethods = map[string]wardenlogical.Factory{
		"jwt":  jwt.Factory,
		"cert": cert.Factory,
	}

	storageBackends = map[string]physical.Factory{
		"inmem_ha": physInmem.NewInmemHA,
		"inmem":    physInmem.NewInmem,
		"postgres": physPostgresql.NewPostgreSQLBackend,
	}
)

func init() {
	ServerCmd.Flags().StringVarP(&configPath, "config", "c", "", "Path to configuration file (e.g., path/to/warden.hcl)")
	ServerCmd.Flags().StringVar(&configDir, "config-dir", "", "Path to a directory of .hcl configuration files, merged in lexical order")
	ServerCmd.Flags().BoolVar(&flagDev, "dev", false, "Enable dev mode: inmem storage, auto-init, auto-unseal")
	ServerCmd.Flags().StringVar(&flagDevRootToken, "dev-root-token", "", "Custom root token for dev mode (any string)")
	ServerCmd.Flags().BoolVar(&flagDevTLS, "dev-tls", false, "Enable TLS for dev mode listener (auto-generates self-signed cert)")
	ServerCmd.Flags().StringVar(&flagDevTLSCertFile, "dev-tls-cert-file", "", "Path to TLS certificate file for dev mode")
	ServerCmd.Flags().StringVar(&flagDevTLSKeyFile, "dev-tls-key-file", "", "Path to TLS private key file for dev mode")
	ServerCmd.Flags().StringVar(&flagDevTLSCACertFile, "dev-tls-ca-cert-file", "", "Path to CA certificate for client verification in dev mode")
	ServerCmd.Flags().BoolVar(&flagDevTLSRequireClientCert, "dev-tls-require-client-cert", false, "Require client certificates when CA cert is configured in dev mode")
}

func run(cmd *cobra.Command, args []string) error {
	// Validate flag combinations
	if flagDevRootToken != "" && !flagDev {
		return fmt.Errorf("-dev-root-token can only be used with -dev")
	}
	if flagDev && configPath != "" {
		return fmt.Errorf("-config cannot be used with -dev (dev mode always uses inmem storage)")
	}
	if flagDev && configDir != "" {
		return fmt.Errorf("-config-dir cannot be used with -dev (dev mode always uses inmem storage)")
	}
	if configPath != "" && configDir != "" {
		return fmt.Errorf("-config and -config-dir are mutually exclusive")
	}

	// Infer -dev-tls from explicit cert file flags
	if flagDevTLSCertFile != "" || flagDevTLSKeyFile != "" || flagDevTLSCACertFile != "" {
		flagDevTLS = true
	}

	// Validate dev-tls flags require -dev
	if flagDevTLS && !flagDev {
		return fmt.Errorf("-dev-tls can only be used with -dev")
	}
	if flagDevTLSRequireClientCert && !flagDev {
		return fmt.Errorf("-dev-tls-require-client-cert can only be used with -dev")
	}

	// Validate cert and key must be provided together
	if (flagDevTLSCertFile != "") != (flagDevTLSKeyFile != "") {
		return fmt.Errorf("-dev-tls-cert-file and -dev-tls-key-file must be provided together")
	}

	// Validate require-client-cert needs a CA cert
	if flagDevTLSRequireClientCert && flagDevTLSCACertFile == "" {
		return fmt.Errorf("-dev-tls-require-client-cert requires -dev-tls-ca-cert-file")
	}

	// Load configuration: dev mode builds defaults, otherwise requires config file or dir
	var conf *config.Config
	switch {
	case flagDev:
		conf = config.DevConfig()
	case configDir != "":
		var err error
		conf, err = config.LoadConfigDirWithLogger(configDir, config.StderrWarner{})
		if err != nil {
			return fmt.Errorf("failed to load config dir: %w", err)
		}
	case configPath != "":
		if _, err := os.Stat(configPath); os.IsNotExist(err) {
			return fmt.Errorf("config file not found: %s", configPath)
		}
		var err error
		conf, err = config.LoadConfigWithLogger(configPath, config.StderrWarner{})
		if err != nil {
			return fmt.Errorf("failed to load config: %w", err)
		}
	default:
		return fmt.Errorf("a config file path or config dir is required. Use -c/-config or -config-dir")
	}

	// Configure TLS for dev mode
	var devTLSCertDir string
	if flagDevTLS {
		certFile, keyFile := flagDevTLSCertFile, flagDevTLSKeyFile
		if certFile == "" && keyFile == "" {
			// Auto-generate self-signed certificate
			var err error
			certFile, keyFile, devTLSCertDir, err = generateDevTLSCert()
			if err != nil {
				return fmt.Errorf("failed to generate dev TLS certificate: %w", err)
			}
			defer func() {
				if devTLSCertDir != "" {
					os.RemoveAll(devTLSCertDir)
				}
			}()
		}
		conf.Listeners[0].TLSDisable = false
		conf.Listeners[0].TLSCertFile = certFile
		conf.Listeners[0].TLSKeyFile = keyFile
		if flagDevTLSCACertFile != "" {
			conf.Listeners[0].TLSClientCAFile = flagDevTLSCACertFile
			requireClientCert := flagDevTLSRequireClientCert
			conf.Listeners[0].TLSRequireClientCert = &requireClientCert
		}
	}

	// construct the logger with gate closed during initialization
	logger := buildGatedLogger(conf)

	// craft the storage
	storage, err := buildStorage(conf, logger)
	if err != nil {
		return fmt.Errorf("failed to construct the storage: %w", err)
	}

	infoKeys := make([]string, 0, 10)
	info := make(map[string]string)
	info["log level"] = conf.LogLevel
	infoKeys = append(infoKeys, "log level")
	info["log file"] = conf.LogFile
	infoKeys = append(infoKeys, "log file")
	info["log format"] = conf.LogFormat
	infoKeys = append(infoKeys, "log format")
	info["log rotate max files"] = fmt.Sprintf("%d", conf.LogRotateMaxFiles)
	infoKeys = append(infoKeys, "log rotate max files")
	info["log rotate max size"] = fmt.Sprintf("%d", conf.LogRotateMegabytes)
	infoKeys = append(infoKeys, "log rotate max size")
	info["log rotation period"] = fmt.Sprintf("%d", conf.LogRotationPeriod)
	infoKeys = append(infoKeys, "log rotation period")

	// returns a slice of env vars formatted as "key=value"
	envVars := os.Environ()
	var envVarKeys []string
	for _, v := range envVars {
		splitEnvVars := strings.Split(v, "=")
		envVarKeys = append(envVarKeys, splitEnvVars[0])
	}

	sort.Strings(envVarKeys)

	key := "environment variables"
	info[key] = strings.Join(envVarKeys, ", ")
	infoKeys = append(infoKeys, key)

	barrierSeal, barrierWrapper, unwrapSeal, seals, sealConfigError, err := setSeal(conf, logger, &infoKeys, info)
	// Check error here
	if err != nil {
		return fmt.Errorf("failed to set seal: %w", err)
	}

	for _, seal := range seals {
		// There is always one nil seal. We need to skip it so we don't start an empty Finalize-Seal-Shamir
		// section.
		if seal == nil {
			continue
		}
		seal := seal // capture range variable
		// Ensure that the seal finalizer is called, even if using verify-only
		defer func(seal *core.Seal) {
			err = (*seal).Finalize(context.Background())
			if err != nil {
				fmt.Fprintf(cmd.OutOrStdout(), "error finalizing seals: %v\n", err)
			}
		}(&seal)
	}

	if barrierSeal == nil {
		return fmt.Errorf("could not create barrier seal! Most likely proper Seal configuration information was not set, but no error was generated")
	}

	// prepare a secure random reader for core
	// TODO : improve this, because it uses rand.Reader
	secureRandomReader, err := configutil.CreateSecureRandomReaderFunc(barrierWrapper)
	if err != nil {
		return fmt.Errorf("failed to create the secure random reader: %w", err)
	}

	// Validate HA configuration: cluster_addr and api_addr are required
	// when the storage backend supports HA and clustering is not disabled.
	if ha, ok := storage.(phy.HABackend); ok && ha.HAEnabled() && !conf.DisableClustering {
		if conf.APIAddr == "" {
			return fmt.Errorf("api_addr is required when HA is enabled")
		}
		if conf.ClusterAddr == "" {
			return fmt.Errorf("cluster_addr is required when HA is enabled")
		}
	}

	coreConfig := createCoreConfig(logger, conf, storage, barrierSeal, unwrapSeal, secureRandomReader)

	newCore, newCoreError := core.NewCore(&coreConfig)
	if newCoreError != nil {
		if core.IsFatalError(newCoreError) {
			return fmt.Errorf("error initializing core: %w", newCoreError)
		}
		fmt.Fprintf(cmd.OutOrStdout(), "A non-fatal error occurred during initialization. Please check the logs for more information: %v\n", newCoreError)
	}

	// Dev mode: auto-initialize and auto-unseal
	var devInitResult *core.InitResult
	if flagDev {
		var err error
		devInitResult, err = devModeInit(newCore, flagDevRootToken)
		if err != nil {
			return fmt.Errorf("dev mode initialization failed: %w", err)
		}
	}

	// Compile server information for output later
	info["storage"] = conf.Storage.Type
	infoKeys = append(infoKeys, "storage")

	if conf.APIAddr != "" {
		info["api address"] = conf.APIAddr
		infoKeys = append(infoKeys, "api address")
	}
	if coreConfig.HAPhysical != nil {
		info["HA enabled"] = "true"
		infoKeys = append(infoKeys, "HA enabled")
	} else {
		info["HA enabled"] = "false"
		infoKeys = append(infoKeys, "HA enabled")
	}

	// Create HTTP handler from core
	httpHandler := wardenhttp.Handler(&wardenhttp.HandlerProperties{
		Core:                 newCore,
		Logger:               logger,
		Version:              buildVersion,
		ClusterTLSConfigFunc: newCore.ClusterTLSConfig,
		ForwardingTimeout:    newCore.ClusterConfig().ForwardingTimeout,
	})

	// init the listeners
	lns, err := initListeners(httpHandler, newCore, conf, logger, &infoKeys, &info)
	if err != nil {
		// Error already logged in initListeners
		return err
	}

	// Shutdown error tracking
	var shutdownErrs []error
	var shutdownErrsMu sync.Mutex

	// Make sure we close all listeners from this point on
	listenerCloseFunc := func() {
		fmt.Fprintf(cmd.OutOrStdout(), "Stopping all listeners\n")
		for _, ln := range lns {
			if err := ln.Stop(); err != nil {
				shutdownErrsMu.Lock()
				shutdownErrs = append(shutdownErrs, fmt.Errorf("failed to stop %s listener at %s: %w", ln.Type(), ln.Addr(), err))
				shutdownErrsMu.Unlock()
			} else {
				fmt.Fprintf(cmd.OutOrStdout(), "Listener stopped successfully: type=%s, address=%s\n", ln.Type(), ln.Addr())
			}
		}
	}

	// Use sync.Once to ensure listeners are stopped exactly once, even if called
	// both via defer (on panic/error) and explicitly before core shutdown
	defer cleanupGuard.Do(listenerCloseFunc)

	sort.Strings(infoKeys)
	fmt.Fprintf(cmd.OutOrStdout(), "\n==> Warden server configuration:\n\n")

	titleCaser := cases.Title(language.English, cases.NoLower)

	for _, k := range infoKeys {
		fmt.Fprintf(cmd.OutOrStdout(), "%24s: %s\n", titleCaser.String(k), info[k])
	}

	// Attempt unsealing in a background goroutine. This is needed for when a
	// OpenBao cluster with multiple servers is configured with auto-unseal but is
	// uninitialized. Once one server initializes the storage backend, this
	// goroutine will pick up the unseal keys and unseal this instance.
	// Skip in dev mode since devModeInit already unsealed the core.
	if !flagDev {
		go runUnseal(cmd.Context(), newCore, context.Background())
	}

	if sealConfigError != nil {
		init, err := newCore.InitializedLocally(context.Background())
		if err != nil {
			return fmt.Errorf("error checking if core is initialized: %w", err)
		}
		if init {
			return fmt.Errorf("warden is initialized but no Seal key could be loaded")
		}
	}

	// start the servers (HTTP and Database)
	// Use signal.NotifyContext to cancel on SIGINT/SIGTERM for graceful shutdown
	ctx, cancel := signal.NotifyContext(cmd.Context(), syscall.SIGINT, syscall.SIGTERM)
	defer cancel()

	// Channel to collect all listener errors
	errChan := make(chan error, len(lns))
	var listenerErrs []error
	var listenerErrsMu sync.Mutex
	totalListeners := len(lns)

	for _, ln := range lns {
		wg.Go(func() {
			if err := ln.Start(ctx); err != nil {
				fmt.Fprintf(cmd.OutOrStdout(), "failed to start listener: %v\n", err)
				errChan <- err
			}
		})
	}

	// Print dev mode banner before opening the log gate
	if flagDev && devInitResult != nil {
		printDevBanner(cmd.OutOrStdout(), devInitResult, devTLSCertDir)
	}

	fmt.Fprintf(cmd.OutOrStdout(), "\n==> Warden server started! Log data will stream in below:\n")
	logger.OpenGate()

	// Write out the PID to the file now that server has successfully started

	// Notify systemd that the server is ready (if applicable)

	// Wait for shutdown
	shutdownTriggered := false

	for !shutdownTriggered {
		select {
		case err := <-errChan:
			// Aggregate listener errors
			listenerErrsMu.Lock()
			listenerErrs = append(listenerErrs, err)
			failedCount := len(listenerErrs)
			listenerErrsMu.Unlock()

			fmt.Fprintf(cmd.OutOrStdout(), "Listener error occurred: failed_count=%d, total_listeners=%d\n", failedCount, totalListeners)

			// Only trigger shutdown if ALL listeners have failed
			if failedCount >= totalListeners {
				fmt.Fprintf(cmd.OutOrStdout(), "All listeners have failed, triggering shutdown: failed_count=%d\n", failedCount)
				shutdownTriggered = true
				cancel()
			}
		case <-ctx.Done():
			fmt.Fprintf(cmd.OutOrStdout(), "Warden shutdown triggered\n")
			shutdownTriggered = true
			cancel()
		}
	}

	// Notify systemd that the server is shutting down

	// Stop the listeners so that we don't process further client requests
	cleanupGuard.Do(listenerCloseFunc)

	// Wait for all listener goroutines to finish and collect any remaining errors
	wg.Wait()

	// Collect any remaining errors from errChan (non-blocking)
	close(errChan)
	for err := range errChan {
		listenerErrsMu.Lock()
		listenerErrs = append(listenerErrs, err)
		listenerErrsMu.Unlock()
	}

	// Log aggregated listener errors if any
	if len(listenerErrs) > 0 {
		aggregatedErr := errors.Join(listenerErrs...)
		fmt.Fprintf(cmd.OutOrStdout(), "Listener errors occurred during runtime: %v, error_count=%d\n", aggregatedErr, len(listenerErrs))
	}

	// Shutdown the core
	if err := newCore.Shutdown(); err != nil {
		fmt.Fprintf(cmd.OutOrStdout(), "core shutdown failed: %v\n", err)
		shutdownErrsMu.Lock()
		shutdownErrs = append(shutdownErrs, fmt.Errorf("core shutdown failed: %w", err))
		shutdownErrsMu.Unlock()
	}

	// Report aggregated shutdown errors
	if len(shutdownErrs) > 0 {
		aggregatedShutdownErr := errors.Join(shutdownErrs...)
		fmt.Fprintf(cmd.OutOrStdout(), "Shutdown completed with errors: %v, error_count=%d\n", aggregatedShutdownErr, len(shutdownErrs))
		return aggregatedShutdownErr
	}

	fmt.Fprintf(cmd.OutOrStdout(), "Server shutdown completed successfully\n")
	return nil
}

func buildGatedLogger(config *config.Config) *log.GatedLogger {
	logConfig := &log.Config{
		Level:     log.ParseLogLevel(config.LogLevel),
		Subsystem: subsystemCore,
		FileConfig: &log.FileConfig{
			Filename:   config.LogFile,
			MaxSize:    config.LogRotateMegabytes,
			MaxAge:     config.LogRotationPeriod,
			MaxBackups: config.LogRotateMaxFiles,
		},
		Format:  log.ParseOutPutFormat(config.LogFormat),
		Outputs: []io.Writer{os.Stdout},
	}

	gateConfig := log.GatedWriterConfig{
		Underlying:    os.Stdout,
		InitialState:  log.GateClosed,
		MaxBufferSize: 10 * 1024 * 1024, // 10MB buffer for initialization logs
	}

	gatedLogger, _ := log.NewGatedLogger(logConfig, gateConfig)

	return gatedLogger
}

func buildStorage(config *config.Config, logger *log.GatedLogger) (phy.Backend, error) {
	// Ensure that a storage is provided
	if config.Storage == nil {
		return nil, errors.New("a storage backend must be specified")
	}

	factory, exists := storageBackends[config.Storage.Type]
	if !exists {
		return nil, fmt.Errorf("unknown storage type %s", config.Storage.Type)
	}

	hclogLogger := log.NewHCLogAdapter(logger.WithSubsystem("storage." + config.Storage.Type))

	storage, err := factory(config.Storage.Config(), hclogLogger)

	if err != nil {
		return nil, fmt.Errorf("error initializing storage of type %s: %w", config.Storage.Type, err)
	}

	return storage, nil
}

func initListeners(httpHandler http.Handler, c *core.Core, conf *config.Config, logger *log.GatedLogger, infoKeys *[]string, info *map[string]string) ([]listener.Listener, error) {
	lns := make([]listener.Listener, 0, len(conf.Listeners)+1)

	for _, lnConfig := range conf.Listeners {
		switch lnConfig.Type {
		case listenerTypeTCP, listenerTypeUnix:
			// construct api listener using shared HTTP handler
			ln, err := api.NewApiListener(api.ApiListenerConfig{
				Logger:               logger.WithSystem(subsystemListener),
				Address:              lnConfig.Address,
				TLSCertFile:          lnConfig.TLSCertFile,
				TLSKeyFile:           lnConfig.TLSKeyFile,
				TLSClientCAFile:      lnConfig.TLSClientCAFile,
				TLSDisable:           lnConfig.TLSDisable,
				TLSRequireClientCert: lnConfig.TLSRequireClientCert,
				TrustedProxies:       lnConfig.TrustedProxies,
			}, httpHandler)
			if err != nil {
				return nil, fmt.Errorf("error initializing listener of type %s: %s", lnConfig.Type, err)
			}
			lns = append(lns, ln)

			infoKey := fmt.Sprintf("listener %d", len(lns))
			(*info)[infoKey] = fmt.Sprintf("%s (addr: %q)", lnConfig.Type, lnConfig.Address)
			*infoKeys = append(*infoKeys, infoKey)
		default:
			return nil, fmt.Errorf("unknown listener type: %s", lnConfig.Type)
		}
	}

	// Start a dedicated cluster listener when cluster_addr is configured.
	// This listener uses auto-generated mTLS certificates from the Core's
	// cluster identity for securing inter-node request forwarding.
	if conf.ClusterAddr != "" {
		clusterAddr, err := parseListenAddress(conf.ClusterAddr)
		if err != nil {
			return nil, fmt.Errorf("error parsing cluster_addr %q: %w", conf.ClusterAddr, err)
		}

		cc := c.ClusterConfig()
		clusterLn, err := clusterlistener.NewClusterListener(clusterlistener.ClusterListenerConfig{
			Logger:        logger.WithSystem("cluster"),
			Address:       clusterAddr,
			Handler:       httpHandler,
			TLSConfigFunc: c.ClusterTLSConfig,
			ReadTimeout:   cc.ClusterListenerReadTimeout,
			WriteTimeout:  cc.ClusterListenerWriteTimeout,
		})
		if err != nil {
			return nil, fmt.Errorf("error initializing cluster listener: %w", err)
		}
		lns = append(lns, clusterLn)

		(*info)["cluster address"] = conf.ClusterAddr
		*infoKeys = append(*infoKeys, "cluster address")
		(*info)["cluster listener"] = clusterAddr + " (mTLS auto)"
		*infoKeys = append(*infoKeys, "cluster listener")
	}

	return lns, nil
}

// parseListenAddress extracts the host:port from a URL string.
// e.g., "https://127.0.0.1:8401" → "127.0.0.1:8401"
func parseListenAddress(addr string) (string, error) {
	if strings.Contains(addr, "://") {
		u, err := url.Parse(addr)
		if err != nil {
			return "", err
		}
		return u.Host, nil
	}
	return addr, nil
}

// setSeal return barrierSeal, barrierWrapper, unwrapSeal, and all the created seals from the configs so we can close them in run
// The two errors are the sealConfigError and the regular error
func setSeal(conf *config.Config, logger *log.GatedLogger, infoKeys *[]string, info map[string]string) (core.Seal, wrapping.Wrapper, core.Seal, []core.Seal, error, error) {
	var barrierSeal core.Seal
	var unwrapSeal core.Seal

	var sealConfigError error
	var wrapper wrapping.Wrapper
	var barrierWrapper wrapping.Wrapper
	if flagDev {
		var err error
		access, _ := core.NewTestSeal(nil)
		barrierSeal, err = core.NewAutoSeal(access)
		if err != nil {
			return nil, nil, nil, nil, nil, err
		}
		return barrierSeal, nil, nil, nil, nil, nil
	}

	// Handle the case where no seal is provided
	switch len(conf.Seals) {
	case 0:
		conf.Seals = append(conf.Seals, config.KMS{Type: wrapping.WrapperTypeShamir.String()})
	case 1:
		// If there's only one seal and it's disabled assume they want to
		// migrate to a shamir seal and simply didn't provide it
		if conf.Seals[0].IsDisabled() {
			conf.Seals = append(conf.Seals, config.KMS{Type: wrapping.WrapperTypeShamir.String()})
		}
	}
	createdSeals := make([]core.Seal, len(conf.Seals))
	for _, configSeal := range conf.Seals {
		sealType := configSeal.Type
		if !conf.Seals[0].IsDisabled() && wardenapi.ReadWardenVariable("WARDEN_SEAL_TYPE") != "" {
			sealType = wardenapi.ReadWardenVariable("WARDEN_SEAL_TYPE")
			configSeal.Type = sealType
		}

		var seal core.Seal
		sealLogger := logger.WithSystem(fmt.Sprintf("seal.%s", sealType))
		defaultSeal := core.NewDefaultSeal(wardenseal.NewAccess(aeadwrapper.NewShamirWrapper()))
		var sealInfoKeys []string
		sealInfoMap := map[string]string{}
		wrapper, sealConfigError = configutil.ConfigureWrapper(&configSeal, &sealInfoKeys, &sealInfoMap, sealLogger)
		if sealConfigError != nil {
			if !errwrap.ContainsType(sealConfigError, new(logical.KeyNotFoundError)) {
				return barrierSeal, barrierWrapper, unwrapSeal, createdSeals, sealConfigError, fmt.Errorf(
					"Error parsing Seal configuration: %w", sealConfigError)
			}
		}
		if wrapper == nil {
			seal = defaultSeal
		} else {
			var err error
			seal, err = core.NewAutoSeal(wardenseal.NewAccess(wrapper))
			if err != nil {
				return nil, nil, nil, nil, nil, err
			}
		}
		infoPrefix := ""
		if configSeal.IsDisabled() {
			unwrapSeal = seal
			infoPrefix = "Old "
		} else {
			barrierSeal = seal
			barrierWrapper = wrapper
		}
		for _, k := range sealInfoKeys {
			*infoKeys = append(*infoKeys, infoPrefix+k)
			info[infoPrefix+k] = sealInfoMap[k]
		}
		createdSeals = append(createdSeals, seal)
	}
	return barrierSeal, barrierWrapper, unwrapSeal, createdSeals, sealConfigError, nil
}

func createCoreConfig(logger *log.GatedLogger, conf *config.Config, backend phy.Backend, barrierSeal, unwrapSeal core.Seal, secureRandomReader io.Reader,
) core.CoreConfig {
	// Detect HA capability from the physical backend
	var haPhysical phy.HABackend
	if ha, ok := backend.(phy.HABackend); ok && ha.HAEnabled() {
		haPhysical = ha
	}

	// ClusterAddr is required when HA is enabled (validated in run()).
	// When HA is not enabled, it may be empty — setupCluster handles
	// that case by skipping cluster TLS setup.
	clusterAddr := conf.ClusterAddr

	coreConfig := &core.CoreConfig{
		RawConfig:              conf,
		Physical:               backend,
		RedirectAddr:           conf.APIAddr,
		ClusterAddr:            clusterAddr,
		StorageType:            conf.Storage.Type,
		HAPhysical:             haPhysical,
		Seal:                   barrierSeal,
		UnwrapSeal:             unwrapSeal,
		AuditDevices:           auditDevices,
		DeclarativeAuditDevices: auditConfigToMountEntries(conf.Audits),
		Providers:              providers,
		ProviderSkills:         providerSkills,
		AuthMethods:            authMethods,
		Logger:                 logger,
		SecureRandomReader:     secureRandomReader,
	}
	return *coreConfig
}

// auditConfigToMountEntries translates HCL audit blocks into the MountEntry
// shape Core operates on. Accessor, namespace, and per-device defaults
// (HMAC salt) are filled in by Core.loadAudits — this function only does
// the mechanical translation.
func auditConfigToMountEntries(blocks []config.AuditBlock) []*core.MountEntry {
	if len(blocks) == 0 {
		return nil
	}
	out := make([]*core.MountEntry, 0, len(blocks))
	for i := range blocks {
		b := &blocks[i]
		out = append(out, &core.MountEntry{
			Type:        b.Type,
			Path:        b.Path,
			Description: b.Description,
			Config:      b.Config(),
			Declarative: true,
		})
	}
	return out
}

func runUnseal(cmdContext context.Context, c *core.Core, ctx context.Context) {
	for {
		err := c.UnsealWithStoredKeys(ctx)
		if err == nil {
			return
		}

		if core.IsFatalError(err) {
			c.Logger().Error("error unsealing core",
				log.Err(err),
			)
			return
		}
		c.Logger().Warn("failed to unseal core",
			log.Err(err),
		)

		timer := time.NewTimer(5 * time.Second)
		select {
		case <-cmdContext.Done():
			timer.Stop()
			return
		case <-timer.C:
		}
	}
}
