package server

import (
	"context"
	"crypto/x509"
	"net"
	"net/http"
	"slices"
	"strconv"

	"go.alanpearce.eu/x/listenfd"

	"github.com/alanpearce/certmagic"
	certmagic_redis "github.com/alanpearce/certmagic-storage-redis"
	"github.com/ardanlabs/conf/v3"
	"github.com/libdns/powerdns"
	"gitlab.com/tozd/go/errors"
)

type redisConfig struct {
	Address       string `conf:"required"`
	Username      string `conf:"default:default"`
	Password      string `conf:"required"`
	EncryptionKey string `conf:"required"`
	KeyPrefix     string `conf:"default:certmagic"`
	TLSEnabled    bool   `conf:"default:false,env:TLS_ENABLED"`
	TLSInsecure   bool   `conf:"default:false,env:TLS_INSECURE"`
}

func (s *Server) serveTLS() (err error) {
	log := s.log.Named("tls")

	wildcardDomain := "*." + s.config.WildcardDomain
	certificateDomains := slices.Clone(s.config.Domains)

	certmagic.HTTPPort = s.runtimeConfig.Port
	certmagic.HTTPSPort = s.runtimeConfig.TLSPort
	certmagic.Default.Logger = log.GetLogger().Named("certmagic")
	cfg := certmagic.NewDefault()

	acme := &certmagic.DefaultACME
	acme.Agreed = true
	acme.Email = s.config.Email
	acme.ListenHost = s.runtimeConfig.ListenAddress

	if s.runtimeConfig.Development {
		ca := s.runtimeConfig.ACMECA
		if ca == "" {
			return errors.New("can't enable tls in development without an ACME_CA")
		}

		cp, err := x509.SystemCertPool()
		if err != nil {
			log.Warn("could not get system certificate pool", "error", err)
			cp = x509.NewCertPool()
		}

		if cacert := s.runtimeConfig.ACMECACert; cacert != "" {
			cp.AppendCertsFromPEM([]byte(cacert))
		}

		// caddy's ACME server (step-ca) doesn't specify an OCSP server
		cfg.OCSP.DisableStapling = true

		acme.CA = s.runtimeConfig.ACMECA
		acme.TrustedRoots = cp
		acme.DisableTLSALPNChallenge = true
	} else {
		rc := &redisConfig{}
		_, err = conf.Parse("REDIS", rc)
		if err != nil {
			return errors.WithMessage(err, "could not parse redis config")
		}

		pdns := &powerdns.Provider{}
		_, err = conf.Parse("POWERDNS", pdns)
		if err != nil {
			return errors.WithMessage(err, "could not parse PowerDNS ACME config")
		}

		acme.DNS01Solver = &certmagic.DNS01Solver{
			DNSManager: certmagic.DNSManager{
				DNSProvider: pdns,
				Logger:      cfg.Logger,
			},
		}

		certificateDomains = append(slices.Clone(s.config.Domains), wildcardDomain)

		rs := certmagic_redis.New()
		rs.Address = []string{rc.Address}
		rs.Username = rc.Username
		rs.Password = rc.Password
		rs.EncryptionKey = rc.EncryptionKey
		rs.KeyPrefix = rc.KeyPrefix
		rs.TlsEnabled = rc.TLSEnabled
		rs.TlsInsecure = rc.TLSInsecure

		cfg.Storage = rs
		err = rs.ProvisionCertMagic(context.TODO(), log.GetLogger())
		if err != nil {
			return errors.WithMessage(err, "could not provision redis storage")
		}
	}

	ln, err := listenfd.GetListener(
		1,
		net.JoinHostPort(s.runtimeConfig.ListenAddress, strconv.Itoa(s.runtimeConfig.Port)),
		log.Named("listenfd"),
	)
	if err != nil {
		return errors.WithMessage(err, "could not bind plain socket")
	}

	go func(ln net.Listener, srv *http.Server) {
		httpMux := http.NewServeMux()
		httpMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
			if certmagic.LooksLikeHTTPChallenge(r) &&
				acme.HandleHTTPChallenge(w, r) {
				return
			}
			url := r.URL
			url.Scheme = "https"
			port := s.config.BaseURL.Port()
			if port == "" {
				url.Host = r.Host
			} else {
				host, _, err := net.SplitHostPort(r.Host)
				if err != nil {
					log.Warn("error splitting host and port", "error", err)
					host = r.Host
				}
				url.Host = net.JoinHostPort(host, s.config.BaseURL.Port())
			}
			if slices.Contains(s.config.Domains, r.Host) {
				http.Redirect(w, r, url.String(), http.StatusMovedPermanently)
			} else {
				http.NotFound(w, r)
			}
		})
		srv.Handler = httpMux

		if err := srv.Serve(ln); err != nil && !errors.Is(err, http.ErrServerClosed) {
			log.Error("error in http handler", "error", err)
		}
	}(ln, &http.Server{
		ReadHeaderTimeout: s.ReadHeaderTimeout,
		ReadTimeout:       s.ReadTimeout,
		WriteTimeout:      s.WriteTimeout,
		IdleTimeout:       s.IdleTimeout,
	})

	log.Debug(
		"starting certmagic",
		"http_port",
		s.runtimeConfig.Port,
		"https_port",
		s.runtimeConfig.TLSPort,
	)
	err = cfg.ManageAsync(context.TODO(), certificateDomains)
	if err != nil {
		return errors.WithMessage(err, "could not enable TLS")
	}
	tlsConfig := cfg.TLSConfig()
	tlsConfig.NextProtos = append([]string{"h2", "http/1.1"}, tlsConfig.NextProtos...)

	sln, err := listenfd.GetListenerTLS(
		0,
		net.JoinHostPort(s.runtimeConfig.ListenAddress, strconv.Itoa(s.runtimeConfig.TLSPort)),
		tlsConfig,
		log.Named("listenfd"),
	)
	if err != nil {
		return errors.WithMessage(err, "could not bind tls socket")
	}

	return s.Serve(sln)
}