package caddyhttp

import (
	"fmt"
	"net/http"
	"strconv"
	"strings"

	"github.com/caddyserver/caddy/v2"
	"github.com/caddyserver/caddy/v2/modules/caddytls"
	"github.com/mholt/certmagic"
	"go.uber.org/zap"
)

// AutoHTTPSConfig is used to disable automatic HTTPS
// or certain aspects of it for a specific server.
// HTTPS is enabled automatically and by default when
// qualifying hostnames are available from the config.
type AutoHTTPSConfig struct {
	// If true, automatic HTTPS will be entirely disabled.
	Disabled bool `json:"disable,omitempty"`

	// If true, only automatic HTTP->HTTPS redirects will
	// be disabled.
	DisableRedir bool `json:"disable_redirects,omitempty"`

	// Hosts/domain names listed here will not be included
	// in automatic HTTPS (they will not have certificates
	// loaded nor redirects applied).
	Skip []string `json:"skip,omitempty"`

	// Hosts/domain names listed here will still be enabled
	// for automatic HTTPS (unless in the Skip list), except
	// that certificates will not be provisioned and managed
	// for these names.
	SkipCerts []string `json:"skip_certificates,omitempty"`

	// By default, automatic HTTPS will obtain and renew
	// certificates for qualifying hostnames. However, if
	// a certificate with a matching SAN is already loaded
	// into the cache, certificate management will not be
	// enabled. To force automated certificate management
	// regardless of loaded certificates, set this to true.
	IgnoreLoadedCerts bool `json:"ignore_loaded_certificates,omitempty"`

	domainSet map[string]struct{}
}

// Skipped returns true if name is in skipSlice, which
// should be one of the Skip* fields on ahc.
func (ahc AutoHTTPSConfig) Skipped(name string, skipSlice []string) bool {
	for _, n := range skipSlice {
		if name == n {
			return true
		}
	}
	return false
}

// automaticHTTPSPhase1 provisions all route matchers, determines
// which domain names found in the routes qualify for automatic
// HTTPS, and sets up HTTP->HTTPS redirects. This phase must occur
// at the beginning of provisioning, because it may add routes and
// even servers to the app, which still need to be set up with the
// rest of them.
func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) error {
	// this map will store associations of HTTP listener
	// addresses to the routes that do HTTP->HTTPS redirects
	lnAddrRedirRoutes := make(map[string]Route)

	for srvName, srv := range app.Servers {
		// as a prerequisite, provision route matchers; this is
		// required for all routes on all servers, and must be
		// done before we attempt to do phase 1 of auto HTTPS,
		// since we have to access the decoded host matchers the
		// handlers will be provisioned later
		if srv.Routes != nil {
			err := srv.Routes.ProvisionMatchers(ctx)
			if err != nil {
				return fmt.Errorf("server %s: setting up route matchers: %v", srvName, err)
			}
		}

		// prepare for automatic HTTPS
		if srv.AutoHTTPS == nil {
			srv.AutoHTTPS = new(AutoHTTPSConfig)
		}
		if srv.AutoHTTPS.Disabled {
			continue
		}

		// skip if all listeners use the HTTP port
		if !srv.listenersUseAnyPortOtherThan(app.httpPort()) {
			app.logger.Info("server is listening only on the HTTP port, so no automatic HTTPS will be applied to this server",
				zap.String("server_name", srvName),
				zap.Int("http_port", app.httpPort()),
			)
			srv.AutoHTTPS.Disabled = true
			continue
		}

		defaultConnPolicies := caddytls.ConnectionPolicies{
			&caddytls.ConnectionPolicy{ALPN: defaultALPN},
		}

		// if all listeners are on the HTTPS port, make sure
		// there is at least one TLS connection policy; it
		// should be obvious that they want to use TLS without
		// needing to specify one empty policy to enable it
		if srv.TLSConnPolicies == nil &&
			!srv.listenersUseAnyPortOtherThan(app.httpsPort()) {
			app.logger.Info("server is listening only on the HTTPS port but has no TLS connection policies; adding one to enable TLS",
				zap.String("server_name", srvName),
				zap.Int("https_port", app.httpsPort()),
			)
			srv.TLSConnPolicies = defaultConnPolicies
		}

		// find all qualifying domain names in this server
		srv.AutoHTTPS.domainSet = make(map[string]struct{})
		for routeIdx, route := range srv.Routes {
			for matcherSetIdx, matcherSet := range route.MatcherSets {
				for matcherIdx, m := range matcherSet {
					if hm, ok := m.(*MatchHost); ok {
						for hostMatcherIdx, d := range *hm {
							var err error
							d, err = repl.ReplaceOrErr(d, true, false)
							if err != nil {
								return fmt.Errorf("%s: route %d, matcher set %d, matcher %d, host matcher %d: %v",
									srvName, routeIdx, matcherSetIdx, matcherIdx, hostMatcherIdx, err)
							}
							if certmagic.HostQualifies(d) &&
								!srv.AutoHTTPS.Skipped(d, srv.AutoHTTPS.Skip) {
								srv.AutoHTTPS.domainSet[d] = struct{}{}
							}
						}
					}
				}
			}
		}

		// nothing more to do here if there are no
		// domains that qualify for automatic HTTPS
		if len(srv.AutoHTTPS.domainSet) == 0 {
			continue
		}

		// tell the server to use TLS if it is not already doing so
		if srv.TLSConnPolicies == nil {
			srv.TLSConnPolicies = defaultConnPolicies
		}

		// nothing left to do if auto redirects are disabled
		if srv.AutoHTTPS.DisableRedir {
			continue
		}

		app.logger.Info("enabling automatic HTTP->HTTPS redirects",
			zap.String("server_name", srvName),
		)

		// create HTTP->HTTPS redirects
		for _, addr := range srv.Listen {
			netw, host, port, err := caddy.SplitNetworkAddress(addr)
			if err != nil {
				return fmt.Errorf("%s: invalid listener address: %v", srvName, addr)
			}

			if parts := strings.SplitN(port, "-", 2); len(parts) == 2 {
				port = parts[0]
			}
			redirTo := "https://{http.request.host}"

			if port != strconv.Itoa(app.httpsPort()) {
				redirTo += ":" + port
			}
			redirTo += "{http.request.uri}"

			// build the plaintext HTTP variant of this address
			httpRedirLnAddr := caddy.JoinNetworkAddress(netw, host, strconv.Itoa(app.httpPort()))

			// build the matcher set for this redirect route
			// (note that we happen to bypass Provision and
			// Validate steps for these matcher modules)
			matcherSet := MatcherSet{MatchProtocol("http")}
			if len(srv.AutoHTTPS.Skip) > 0 {
				matcherSet = append(matcherSet, MatchNegate{
					Matchers: MatcherSet{MatchHost(srv.AutoHTTPS.Skip)},
				})
			}

			// create the route that does the redirect and associate
			// it with the listener address it will be served from
			// (note that we happen to bypass any Provision or Validate
			// steps on the handler modules created here)
			lnAddrRedirRoutes[httpRedirLnAddr] = Route{
				MatcherSets: []MatcherSet{matcherSet},
				Handlers: []MiddlewareHandler{
					StaticResponse{
						StatusCode: WeakString(strconv.Itoa(http.StatusPermanentRedirect)),
						Headers: http.Header{
							"Location":   []string{redirTo},
							"Connection": []string{"close"},
						},
						Close: true,
					},
				},
			}
		}
	}

	// if there are HTTP->HTTPS redirects to add, do so now
	if len(lnAddrRedirRoutes) == 0 {
		return nil
	}

	var redirServerAddrs []string
	var redirRoutes RouteList

	// for each redirect listener, see if there's already a
	// server configured to listen on that exact address; if so,
	// simply add the redirect route to the end of its route
	// list; otherwise, we'll create a new server for all the
	// listener addresses that are unused and serve the
	// remaining redirects from it
redirRoutesLoop:
	for addr, redirRoute := range lnAddrRedirRoutes {
		for srvName, srv := range app.Servers {
			if srv.hasListenerAddress(addr) {
				// user has configured a server for the same address
				// that the redirect runs from; simply append our
				// redirect route to the existing routes, with a
				// caveat that their config might override ours
				app.logger.Warn("server is listening on same interface as redirects, so automatic HTTP->HTTPS redirects might be overridden by your own configuration",
					zap.String("server_name", srvName),
					zap.String("interface", addr),
				)
				srv.Routes = append(srv.Routes, redirRoute)
				continue redirRoutesLoop
			}
		}
		// no server with this listener address exists;
		// save this address and route for custom server
		redirServerAddrs = append(redirServerAddrs, addr)
		redirRoutes = append(redirRoutes, redirRoute)
	}

	// if there are routes remaining which do not belong
	// in any existing server, make our own to serve the
	// rest of the redirects
	if len(redirServerAddrs) > 0 {
		app.Servers["remaining_auto_https_redirects"] = &Server{
			Listen: redirServerAddrs,
			Routes: redirRoutes,
		}
	}

	return nil
}

// automaticHTTPSPhase2 attaches a TLS app pointer to each
// server and begins certificate management for all names
// in the qualifying domain set for each server. This phase
// must occur after provisioning, and at the beginning of
// the app start, before starting each of the servers.
func (app *App) automaticHTTPSPhase2() error {
	tlsAppIface, err := app.ctx.App("tls")
	if err != nil {
		return fmt.Errorf("getting tls app: %v", err)
	}
	tlsApp := tlsAppIface.(*caddytls.TLS)

	// set the tlsApp pointer before starting any
	// challenges, since it is required to solve
	// the ACME HTTP challenge
	for _, srv := range app.Servers {
		srv.tlsApp = tlsApp
	}

	// begin managing certificates for enabled servers
	for srvName, srv := range app.Servers {
		if srv.AutoHTTPS == nil ||
			srv.AutoHTTPS.Disabled ||
			len(srv.AutoHTTPS.domainSet) == 0 {
			continue
		}

		// marshal the domains into a slice
		var domains, domainsForCerts []string
		for d := range srv.AutoHTTPS.domainSet {
			domains = append(domains, d)
			if !srv.AutoHTTPS.Skipped(d, srv.AutoHTTPS.SkipCerts) {
				// if a certificate for this name is already loaded,
				// don't obtain another one for it, unless we are
				// supposed to ignore loaded certificates
				if !srv.AutoHTTPS.IgnoreLoadedCerts &&
					len(tlsApp.AllMatchingCertificates(d)) > 0 {
					app.logger.Info("skipping automatic certificate management because one or more matching certificates are already loaded",
						zap.String("domain", d),
						zap.String("server_name", srvName),
					)
					continue
				}
				domainsForCerts = append(domainsForCerts, d)
			}
		}

		// ensure that these certificates are managed properly;
		// for example, it's implied that the HTTPPort should also
		// be the port the HTTP challenge is solved on, and so
		// for HTTPS port and TLS-ALPN challenge also - we need
		// to tell the TLS app to manage these certs by honoring
		// those port configurations
		acmeManager := &caddytls.ACMEManagerMaker{
			Challenges: &caddytls.ChallengesConfig{
				HTTP: &caddytls.HTTPChallengeConfig{
					AlternatePort: app.HTTPPort, // we specifically want the user-configured port, if any
				},
				TLSALPN: &caddytls.TLSALPNChallengeConfig{
					AlternatePort: app.HTTPSPort, // we specifically want the user-configured port, if any
				},
			},
		}
		if tlsApp.Automation == nil {
			tlsApp.Automation = new(caddytls.AutomationConfig)
		}
		tlsApp.Automation.Policies = append(tlsApp.Automation.Policies,
			caddytls.AutomationPolicy{
				Hosts:      domainsForCerts,
				Management: acmeManager,
			})

		// manage their certificates
		app.logger.Info("enabling automatic TLS certificate management",
			zap.Strings("domains", domainsForCerts),
		)
		err := tlsApp.Manage(domainsForCerts)
		if err != nil {
			return fmt.Errorf("%s: managing certificate for %s: %s", srvName, domains, err)
		}
	}

	return nil
}