//go:build with_acme

package tls

import (
	"context"
	"crypto/tls"
	"os"
	"slices"
	"strings"

	"github.com/sagernet/sing-box/adapter"
	C "github.com/sagernet/sing-box/constant"
	"github.com/sagernet/sing-box/option"
	E "github.com/sagernet/sing/common/exceptions"
	"github.com/sagernet/sing/common/logger"
	"github.com/sagernet/sing/service/filemanager"

	"github.com/caddyserver/certmagic"
	"github.com/libdns/acmedns"
	"github.com/libdns/alidns"
	"github.com/libdns/cloudflare"
	"github.com/mholt/acmez/v3/acme"
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
)

type acmeWrapper struct {
	ctx           context.Context
	cfg           *certmagic.Config
	cache         *certmagic.Cache
	zapLogger     *zap.Logger
	dataDirectory string
	domain        []string
}

func (w *acmeWrapper) Start() error {
	if w.dataDirectory != "" {
		err := filemanager.MkdirAll(w.ctx, w.dataDirectory, 0o700)
		if err != nil {
			return E.Cause(err, "create ACME data directory")
		}
	}
	config := w.cfg
	cache := certmagic.NewCache(certmagic.CacheOptions{
		GetConfigForCert: func(certificate certmagic.Certificate) (*certmagic.Config, error) {
			return config, nil
		},
		Logger: w.zapLogger,
	})
	config = certmagic.New(cache, *config)
	w.cfg = config
	w.cache = cache
	return w.cfg.ManageSync(w.ctx, w.domain)
}

func (w *acmeWrapper) Close() error {
	if w.cache != nil {
		w.cache.Stop()
	}
	return nil
}

func (w *acmeWrapper) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
	return w.cfg.GetCertificate(hello)
}

func startACME(ctx context.Context, logger logger.Logger, options option.InboundACMEOptions) (*tls.Config, adapter.SimpleLifecycle, error) {
	var acmeServer string
	switch options.Provider {
	case "", "letsencrypt":
		acmeServer = certmagic.LetsEncryptProductionCA
	case "zerossl":
		acmeServer = certmagic.ZeroSSLProductionCA
	default:
		if !strings.HasPrefix(options.Provider, "https://") {
			return nil, nil, E.New("unsupported acme provider: " + options.Provider)
		}
		acmeServer = options.Provider
	}
	var (
		storage       certmagic.Storage
		dataDirectory string
	)
	if options.DataDirectory != "" {
		dataDirectory = filemanager.BasePath(ctx, os.ExpandEnv(options.DataDirectory))
		storage = &certmagic.FileStorage{
			Path: dataDirectory,
		}
	} else {
		storage = certmagic.Default.Storage
	}
	zapLogger := zap.New(zapcore.NewCore(
		zapcore.NewConsoleEncoder(ACMEEncoderConfig()),
		&ACMELogWriter{Logger: logger},
		zap.DebugLevel,
	))
	config := &certmagic.Config{
		DefaultServerName: options.DefaultServerName,
		Storage:           storage,
		Logger:            zapLogger,
	}
	profile := options.Profile
	if profile == "" && acmeServer == certmagic.LetsEncryptProductionCA && slices.ContainsFunc(options.Domain, certmagic.SubjectIsIP) {
		profile = "shortlived"
	}

	acmeConfig := certmagic.ACMEIssuer{
		CA:                      acmeServer,
		Email:                   options.Email,
		Agreed:                  true,
		Profile:                 profile,
		DisableHTTPChallenge:    options.DisableHTTPChallenge,
		DisableTLSALPNChallenge: options.DisableTLSALPNChallenge,
		AltHTTPPort:             int(options.AlternativeHTTPPort),
		AltTLSALPNPort:          int(options.AlternativeTLSPort),
		Logger:                  zapLogger,
	}
	if dnsOptions := options.DNS01Challenge; dnsOptions != nil && dnsOptions.Provider != "" {
		var solver certmagic.DNS01Solver
		switch dnsOptions.Provider {
		case C.DNSProviderAliDNS:
			solver.DNSProvider = &alidns.Provider{
				CredentialInfo: alidns.CredentialInfo{
					AccessKeyID:     dnsOptions.AliDNSOptions.AccessKeyID,
					AccessKeySecret: dnsOptions.AliDNSOptions.AccessKeySecret,
					RegionID:        dnsOptions.AliDNSOptions.RegionID,
					SecurityToken:   dnsOptions.AliDNSOptions.SecurityToken,
				},
			}
		case C.DNSProviderCloudflare:
			solver.DNSProvider = &cloudflare.Provider{
				APIToken:  dnsOptions.CloudflareOptions.APIToken,
				ZoneToken: dnsOptions.CloudflareOptions.ZoneToken,
			}
		case C.DNSProviderACMEDNS:
			solver.DNSProvider = &acmedns.Provider{
				Username:  dnsOptions.ACMEDNSOptions.Username,
				Password:  dnsOptions.ACMEDNSOptions.Password,
				Subdomain: dnsOptions.ACMEDNSOptions.Subdomain,
				ServerURL: dnsOptions.ACMEDNSOptions.ServerURL,
			}
		default:
			return nil, nil, E.New("unsupported ACME DNS01 provider type: " + dnsOptions.Provider)
		}
		acmeConfig.DNS01Solver = &solver
	}
	if options.ExternalAccount != nil && options.ExternalAccount.KeyID != "" {
		acmeConfig.ExternalAccount = (*acme.EAB)(options.ExternalAccount)
	}
	config.Issuers = []certmagic.Issuer{certmagic.NewACMEIssuer(config, acmeConfig)}
	wrapper := &acmeWrapper{
		ctx:           ctx,
		cfg:           config,
		zapLogger:     zapLogger,
		dataDirectory: dataDirectory,
		domain:        options.Domain,
	}
	var tlsConfig *tls.Config
	if acmeConfig.DisableTLSALPNChallenge || acmeConfig.DNS01Solver != nil {
		tlsConfig = &tls.Config{
			GetCertificate: wrapper.GetCertificate,
		}
	} else {
		tlsConfig = &tls.Config{
			GetCertificate: wrapper.GetCertificate,
			NextProtos:     []string{C.ACMETLS1Protocol},
		}
	}
	return tlsConfig, wrapper, nil
}
