package commands

import (
	"encoding/json"
	"fmt"
	"os"
	"regexp"
	"strings"

	"github.com/StackExchange/dnscontrol/v3/models"
	"github.com/StackExchange/dnscontrol/v3/pkg/acme"
	"github.com/StackExchange/dnscontrol/v3/pkg/credsfile"
	"github.com/StackExchange/dnscontrol/v3/pkg/normalize"
	"github.com/StackExchange/dnscontrol/v3/pkg/printer"
	"github.com/urfave/cli/v2"
)

var _ = cmd(catUtils, func() *cli.Command {
	var args GetCertsArgs
	return &cli.Command{
		Name:  "get-certs",
		Usage: "Issue certificates via Let's Encrypt",
		Action: func(c *cli.Context) error {
			return exit(GetCerts(args))
		},
		Flags: args.flags(),
	}
}())

// GetCertsArgs stores the flags and arguments common to cert commands
type GetCertsArgs struct {
	GetDNSConfigArgs
	GetCredentialsArgs

	ACMEServer     string
	CertsFile      string
	RenewUnderDays int
	CertDirectory  string
	Email          string
	AgreeTOS       bool
	Verbose        bool
	Vault          bool
	VaultPath      string
	Only           string

	Notify bool

	IgnoredProviders string
}

func (args *GetCertsArgs) flags() []cli.Flag {
	flags := args.GetDNSConfigArgs.flags()
	flags = append(flags, args.GetCredentialsArgs.flags()...)

	flags = append(flags, &cli.StringFlag{
		Name:        "acme",
		Destination: &args.ACMEServer,
		Value:       "live",
		Usage:       `ACME server to issue against. Give full directory endpoint. Can also use 'staging' or 'live' for standard Let's Encrypt endpoints.`,
	})
	flags = append(flags, &cli.IntFlag{
		Name:        "renew",
		Destination: &args.RenewUnderDays,
		Value:       15,
		Usage:       `Renew certs with less than this many days remaining`,
	})
	flags = append(flags, &cli.StringFlag{
		Name:        "dir",
		Destination: &args.CertDirectory,
		Value:       ".",
		Usage:       `Directory to store certificates and other data`,
	})
	flags = append(flags, &cli.StringFlag{
		Name:        "certConfig",
		Destination: &args.CertsFile,
		Value:       "certs.json",
		Usage:       `Json file containing list of certificates to issue`,
	})
	flags = append(flags, &cli.StringFlag{
		Name:        "email",
		Destination: &args.Email,
		Value:       "",
		Usage:       `Email to register with let's encrypt`,
	})
	flags = append(flags, &cli.BoolFlag{
		Name:        "agreeTOS",
		Destination: &args.AgreeTOS,
		Usage:       `Must provide this to agree to Let's Encrypt terms of service`,
	})
	flags = append(flags, &cli.BoolFlag{
		Name:        "vault",
		Destination: &args.Vault,
		Usage:       `Store certificates as secrets in hashicorp vault instead of on disk.`,
	})
	flags = append(flags, &cli.StringFlag{
		Name:        "vaultPath",
		Destination: &args.VaultPath,
		Value:       "/secret/certs",
		Usage:       `Path in vault to store certificates`,
	})
	flags = append(flags, &cli.StringFlag{
		Name:        "skip",
		Destination: &args.IgnoredProviders,
		Value:       "",
		Usage:       `Provider names to not use for challenges (comma separated)`,
	})
	flags = append(flags, &cli.BoolFlag{
		Name:        "verbose",
		Destination: &args.Verbose,
		Usage:       "Enable detailed logging (deprecated: use the global -v flag)",
	})
	flags = append(flags, &cli.BoolFlag{
		Name:        "notify",
		Destination: &args.Notify,
		Usage:       `set to true to send notifications to configured destinations`,
	})
	flags = append(flags, &cli.StringFlag{
		Name:        "only",
		Destination: &args.Only,
		Usage:       `Only check a single cert. Provide cert name.`,
	})
	return flags
}

// GetCerts implements the get-certs command.
func GetCerts(args GetCertsArgs) error {
	fmt.Println(args.JSFile)
	// check agree flag
	if !args.AgreeTOS {
		return fmt.Errorf("you must agree to the Let's Encrypt Terms of Service by using -agreeTOS")
	}
	if args.Email == "" {
		return fmt.Errorf("must provide email to use for Let's Encrypt registration")
	}

	// load dns config
	cfg, err := GetDNSConfig(args.GetDNSConfigArgs)
	if err != nil {
		return err
	}
	errs := normalize.ValidateAndNormalizeConfig(cfg)
	if PrintValidationErrors(errs) {
		return fmt.Errorf("exiting due to validation errors")
	}
	providerConfigs, err := credsfile.LoadProviderConfigs(args.CredsFile)
	if err != nil {
		return err
	}
	notifier, err := InitializeProviders(cfg, providerConfigs, args.Notify)
	if err != nil {
		return err
	}

	for _, skip := range strings.Split(args.IgnoredProviders, ",") {
		acme.IgnoredProviders[skip] = true
	}

	// load cert list
	certList := []*acme.CertConfig{}
	f, err := os.Open(args.CertsFile)
	if err != nil {
		return err
	}
	defer f.Close()
	dec := json.NewDecoder(f)
	err = dec.Decode(&certList)
	if err != nil {
		return err
	}
	if len(certList) == 0 {
		return fmt.Errorf("must provide at least one certificate to issue in cert configuration")
	}
	if err = validateCertificateList(certList, cfg); err != nil {
		return err
	}

	acmeServer := args.ACMEServer
	if acmeServer == "live" {
		acmeServer = acme.LetsEncryptLive
	} else if acmeServer == "staging" {
		acmeServer = acme.LetsEncryptStage
	}

	var client acme.Client

	if args.Vault {
		client, err = acme.NewVault(cfg, args.VaultPath, args.Email, acmeServer, notifier)
	} else {
		client, err = acme.New(cfg, args.CertDirectory, args.Email, acmeServer, notifier)
	}
	if err != nil {
		return err
	}
	var manyerr error
	for _, cert := range certList {
		if args.Only != "" && cert.CertName != args.Only {
			continue
		}
		v := args.Verbose || printer.DefaultPrinter.Verbose
		issued, err := client.IssueOrRenewCert(cert, args.RenewUnderDays, v)
		if issued || err != nil {
			notifier.Notify(cert.CertName, "certificate", "Issued new certificate", err, false)
		}
		if err != nil {
			if manyerr == nil {
				manyerr = err
			} else {
				manyerr = fmt.Errorf("%w; %v", manyerr, err)
			}
		}
	}
	notifier.Done()
	return manyerr
}

var validCertNamesRegex = regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9_\-]*$`)

func validateCertificateList(certs []*acme.CertConfig, cfg *models.DNSConfig) error {
	for _, cert := range certs {
		name := cert.CertName
		if !validCertNamesRegex.MatchString(name) {
			return fmt.Errorf("'%s' is not a valid certificate name. Only alphanumerics, - and _ allowed", name)
		}
		sans := cert.Names
		if len(sans) > 100 {
			return fmt.Errorf("certificate '%s' has too many SANs. Max of 100", name)
		}
		if len(sans) == 0 {
			return fmt.Errorf("certificate '%s' needs at least one SAN", name)
		}
		for _, san := range sans {
			d := cfg.DomainContainingFQDN(san)
			if d == nil {
				return fmt.Errorf("DNS config has no domain that matches SAN '%s'", san)
			}
		}
	}
	return nil
}
