package notmain

import (
	"context"
	"flag"
	"fmt"
	"net/http"
	"net/url"
	"os"
	"strings"
	"time"

	"github.com/prometheus/client_golang/prometheus"
	"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"

	"github.com/letsencrypt/boulder/cmd"
	"github.com/letsencrypt/boulder/config"
	"github.com/letsencrypt/boulder/db"
	"github.com/letsencrypt/boulder/features"
	bgrpc "github.com/letsencrypt/boulder/grpc"
	"github.com/letsencrypt/boulder/issuance"
	blog "github.com/letsencrypt/boulder/log"
	"github.com/letsencrypt/boulder/metrics/measured_http"
	"github.com/letsencrypt/boulder/ocsp/responder"
	"github.com/letsencrypt/boulder/ocsp/responder/live"
	redis_responder "github.com/letsencrypt/boulder/ocsp/responder/redis"
	rapb "github.com/letsencrypt/boulder/ra/proto"
	rocsp_config "github.com/letsencrypt/boulder/rocsp/config"
	"github.com/letsencrypt/boulder/sa"
	sapb "github.com/letsencrypt/boulder/sa/proto"
)

type Config struct {
	OCSPResponder struct {
		DebugAddr string       `validate:"omitempty,hostname_port"`
		DB        cmd.DBConfig `validate:"required_without_all=Source SAService,structonly"`

		// Source indicates the source of pre-signed OCSP responses to be used. It
		// can be a DBConnect string or a file URL. The file URL style is used
		// when responding from a static file for intermediates and roots.
		// If DBConfig has non-empty fields, it takes precedence over this.
		Source string `validate:"required_without_all=DB.DBConnectFile SAService Redis"`

		// The list of issuer certificates, against which OCSP requests/responses
		// are checked to ensure we're not responding for anyone else's certs.
		IssuerCerts []string `validate:"min=1,dive,required"`

		Path string

		// ListenAddress is the address:port on which to listen for incoming
		// OCSP requests. This has a default value of ":80".
		ListenAddress string `validate:"omitempty,hostname_port"`

		// When to timeout a request. This should be slightly lower than the
		// upstream's timeout when making request to ocsp-responder.
		Timeout config.Duration `validate:"-"`

		// How often a response should be signed when using Redis/live-signing
		// path. This has a default value of 60h.
		LiveSigningPeriod config.Duration `validate:"-"`

		// A limit on how many requests to the RA (and onwards to the CA) will
		// be made to sign responses that are not fresh in the cache. This
		// should be set to somewhat less than
		// (HSM signing capacity) / (number of ocsp-responders).
		// Requests that would exceed this limit will block until capacity is
		// available and eventually serve an HTTP 500 Internal Server Error.
		// This has a default value of 1000.
		MaxInflightSignings int `validate:"min=0"`

		// A limit on how many goroutines can be waiting for a signing slot at
		// a time. When this limit is exceeded, additional signing requests
		// will immediately serve an HTTP 500 Internal Server Error until
		// we are back below the limit. This provides load shedding for when
		// inbound requests arrive faster than our ability to sign them.
		// The default of 0 means "no limit." A good value for this is the
		// longest queue we can expect to process before a timeout. For
		// instance, if the timeout is 5 seconds, and a signing takes 20ms,
		// and we have MaxInflightSignings = 40, we can expect to process
		// 40 * 5 / 0.02 = 10,000 requests before the oldest request times out.
		MaxSigningWaiters int `validate:"min=0"`

		ShutdownStopTimeout config.Duration

		RequiredSerialPrefixes []string `validate:"omitempty,dive,hexadecimal"`

		Features features.Config

		// Configuration for using Redis as a cache. This configuration should
		// allow for both read and write access.
		Redis *rocsp_config.RedisConfig `validate:"required_without=Source"`

		// TLS client certificate, private key, and trusted root bundle.
		TLS cmd.TLSConfig `validate:"required_without=Source,structonly"`

		// RAService configures how to communicate with the RA when it is necessary
		// to generate a fresh OCSP response.
		RAService *cmd.GRPCClientConfig

		// SAService configures how to communicate with the SA to look up
		// certificate status metadata used to confirm/deny that the response from
		// Redis is up-to-date.
		SAService *cmd.GRPCClientConfig `validate:"required_without_all=DB.DBConnectFile Source"`

		// LogSampleRate sets how frequently error logs should be emitted. This
		// avoids flooding the logs during outages. 1 out of N log lines will be emitted.
		// If LogSampleRate is 0, no logs will be emitted.
		LogSampleRate int `validate:"min=0"`
	}

	Syslog        cmd.SyslogConfig
	OpenTelemetry cmd.OpenTelemetryConfig

	// OpenTelemetryHTTPConfig configures tracing on incoming HTTP requests
	OpenTelemetryHTTPConfig cmd.OpenTelemetryHTTPConfig
}

func main() {
	listenAddr := flag.String("addr", "", "OCSP listen address override")
	debugAddr := flag.String("debug-addr", "", "Debug server address override")
	configFile := flag.String("config", "", "File path to the configuration file for this service")
	flag.Parse()

	if *configFile == "" {
		fmt.Fprintf(os.Stderr, `Usage of %s:
Config JSON should contain either a DBConnectFile or a Source value containing a file: URL.
If Source is a file: URL, the file should contain a list of OCSP responses in base64-encoded DER,
as generated by Boulder's ceremony command.
`, os.Args[0])
		flag.PrintDefaults()
		os.Exit(1)
	}

	var c Config
	err := cmd.ReadConfigFile(*configFile, &c)
	cmd.FailOnError(err, "Reading JSON config file into config structure")

	features.Set(c.OCSPResponder.Features)

	if *listenAddr != "" {
		c.OCSPResponder.ListenAddress = *listenAddr
	}
	if *debugAddr != "" {
		c.OCSPResponder.DebugAddr = *debugAddr
	}

	scope, logger, oTelShutdown := cmd.StatsAndLogging(c.Syslog, c.OpenTelemetry, c.OCSPResponder.DebugAddr)
	logger.Info(cmd.VersionString())

	clk := cmd.Clock()

	var source responder.Source

	if strings.HasPrefix(c.OCSPResponder.Source, "file:") {
		url, err := url.Parse(c.OCSPResponder.Source)
		cmd.FailOnError(err, "Source was not a URL")
		filename := url.Path
		// Go interprets cwd-relative file urls (file:test/foo.txt) as having the
		// relative part of the path in the 'Opaque' field.
		if filename == "" {
			filename = url.Opaque
		}
		source, err = responder.NewMemorySourceFromFile(filename, logger)
		cmd.FailOnError(err, fmt.Sprintf("Couldn't read file: %s", url.Path))
	} else {
		// Set up the redis source and the combined multiplex source.
		rocspRWClient, err := rocsp_config.MakeClient(c.OCSPResponder.Redis, clk, scope)
		cmd.FailOnError(err, "Could not make redis client")

		err = rocspRWClient.Ping(context.Background())
		cmd.FailOnError(err, "pinging Redis")

		liveSigningPeriod := c.OCSPResponder.LiveSigningPeriod.Duration
		if liveSigningPeriod == 0 {
			liveSigningPeriod = 60 * time.Hour
		}

		tlsConfig, err := c.OCSPResponder.TLS.Load(scope)
		cmd.FailOnError(err, "TLS config")

		raConn, err := bgrpc.ClientSetup(c.OCSPResponder.RAService, tlsConfig, scope, clk)
		cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to RA")
		rac := rapb.NewRegistrationAuthorityClient(raConn)

		maxInflight := c.OCSPResponder.MaxInflightSignings
		if maxInflight == 0 {
			maxInflight = 1000
		}
		liveSource := live.New(rac, int64(maxInflight), c.OCSPResponder.MaxSigningWaiters)

		rocspSource, err := redis_responder.NewRedisSource(rocspRWClient, liveSource, liveSigningPeriod, clk, scope, logger, c.OCSPResponder.LogSampleRate)
		cmd.FailOnError(err, "Could not create redis source")

		var dbMap *db.WrappedMap
		if c.OCSPResponder.DB != (cmd.DBConfig{}) {
			dbMap, err = sa.InitWrappedDb(c.OCSPResponder.DB, scope, logger)
			cmd.FailOnError(err, "While initializing dbMap")
		}

		var sac sapb.StorageAuthorityReadOnlyClient
		if c.OCSPResponder.SAService != nil {
			saConn, err := bgrpc.ClientSetup(c.OCSPResponder.SAService, tlsConfig, scope, clk)
			cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to SA")
			sac = sapb.NewStorageAuthorityReadOnlyClient(saConn)
		}

		source, err = redis_responder.NewCheckedRedisSource(rocspSource, dbMap, sac, scope, logger)
		cmd.FailOnError(err, "Could not create checkedRedis source")
	}

	// Load the certificate from the file path.
	issuerCerts := make([]*issuance.Certificate, len(c.OCSPResponder.IssuerCerts))
	for i, issuerFile := range c.OCSPResponder.IssuerCerts {
		issuerCert, err := issuance.LoadCertificate(issuerFile)
		cmd.FailOnError(err, "Could not load issuer cert")
		issuerCerts[i] = issuerCert
	}

	source, err = responder.NewFilterSource(
		issuerCerts,
		c.OCSPResponder.RequiredSerialPrefixes,
		source,
		scope,
		logger,
		clk,
	)
	cmd.FailOnError(err, "Could not create filtered source")

	m := mux(c.OCSPResponder.Path, source, c.OCSPResponder.Timeout.Duration, scope, c.OpenTelemetryHTTPConfig.Options(), logger, c.OCSPResponder.LogSampleRate)

	if c.OCSPResponder.ListenAddress == "" {
		cmd.Fail("HTTP listen address is not configured")
	}

	logger.Infof("HTTP server listening on %s", c.OCSPResponder.ListenAddress)

	srv := &http.Server{
		ReadTimeout:  30 * time.Second,
		WriteTimeout: 120 * time.Second,
		IdleTimeout:  120 * time.Second,
		Addr:         c.OCSPResponder.ListenAddress,
		Handler:      m,
	}

	err = srv.ListenAndServe()
	if err != nil && err != http.ErrServerClosed {
		cmd.FailOnError(err, "Running HTTP server")
	}

	// When main is ready to exit (because it has received a shutdown signal),
	// gracefully shutdown the servers. Calling these shutdown functions causes
	// ListenAndServe() to immediately return, cleaning up the server goroutines
	// as well, then waits for any lingering connection-handing goroutines to
	// finish and clean themselves up.
	defer func() {
		ctx, cancel := context.WithTimeout(context.Background(),
			c.OCSPResponder.ShutdownStopTimeout.Duration)
		defer cancel()
		_ = srv.Shutdown(ctx)
		oTelShutdown(ctx)
	}()

	cmd.WaitForSignal()
}

// ocspMux partially implements the interface defined for http.ServeMux but doesn't implement
// the path cleaning its Handler method does. Notably http.ServeMux will collapse repeated
// slashes into a single slash which breaks the base64 encoding that is used in OCSP GET
// requests. ocsp.Responder explicitly recommends against using http.ServeMux
// for this reason.
type ocspMux struct {
	handler http.Handler
}

func (om *ocspMux) Handler(_ *http.Request) (http.Handler, string) {
	return om.handler, "/"
}

func mux(responderPath string, source responder.Source, timeout time.Duration, stats prometheus.Registerer, oTelHTTPOptions []otelhttp.Option, logger blog.Logger, sampleRate int) http.Handler {
	stripPrefix := http.StripPrefix(responderPath, responder.NewResponder(source, timeout, stats, logger, sampleRate))
	h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Method == "GET" && r.URL.Path == "/" {
			w.Header().Set("Cache-Control", "max-age=43200") // Cache for 12 hours
			w.WriteHeader(200)
			return
		}
		stripPrefix.ServeHTTP(w, r)
	})
	return measured_http.New(&ocspMux{h}, cmd.Clock(), stats, oTelHTTPOptions...)
}

func init() {
	cmd.RegisterCommand("ocsp-responder", main, &cmd.ConfigValidator{Config: &Config{}})
}
