package acme

// Some light reading:
// https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert

import (
	"fmt"
	"os"
	"os/exec"
	"strings"

	"npm/internal/config"
	"npm/internal/entity/certificateauthority"
	"npm/internal/entity/dnsprovider"
	"npm/internal/logger"
)

func getAcmeShFilePath() (string, error) {
	path, err := exec.LookPath("acme.sh")
	if err != nil {
		return path, fmt.Errorf("Cannot find acme.sh execuatable script in PATH")
	}
	return path, nil
}

func getCommonEnvVars() []string {
	return []string{
		fmt.Sprintf("ACMESH_CONFIG_HOME=%s", os.Getenv("ACMESH_CONFIG_HOME")),
		fmt.Sprintf("ACMESH_HOME=%s", os.Getenv("ACMESH_HOME")),
		fmt.Sprintf("CERT_HOME=%s", os.Getenv("CERT_HOME")),
		fmt.Sprintf("LE_CONFIG_HOME=%s", os.Getenv("LE_CONFIG_HOME")),
		fmt.Sprintf("LE_WORKING_DIR=%s", os.Getenv("LE_WORKING_DIR")),
	}
}

// GetAcmeShVersion will return the acme.sh script version
func GetAcmeShVersion() string {
	if r, err := shExec([]string{"--version"}, nil); err == nil {
		// modify the output
		r = strings.Trim(r, "\n")
		v := strings.Split(r, "\n")
		return v[len(v)-1]
	}
	return ""
}

// CreateAccountKey is required for each server initially
func CreateAccountKey(ca *certificateauthority.Model) error {
	args := []string{"--create-account-key", "--accountkeylength", "2048"}
	if ca != nil {
		logger.Info("Acme.sh CreateAccountKey for %s", ca.AcmeshServer)
		args = append(args, "--server", ca.AcmeshServer)
		if ca.CABundle != "" {
			args = append(args, "--ca-bundle", ca.CABundle)
		}
	} else {
		logger.Info("Acme.sh CreateAccountKey")
	}

	args = append(args, getCommonArgs()...)
	ret, err := shExec(args, nil)
	if err != nil {
		return err
	}

	logger.Debug("CreateAccountKey returned:\n%+v", ret)

	return nil
}

// RequestCert does all the heavy lifting
func RequestCert(domains []string, method, outputFullchainFile, outputKeyFile string, dnsProvider *dnsprovider.Model, ca *certificateauthority.Model, force bool) (string, error) {
	args, err := buildCertRequestArgs(domains, method, outputFullchainFile, outputKeyFile, dnsProvider, ca, force)
	if err != nil {
		return err.Error(), err
	}

	envs := make([]string, 0)
	if dnsProvider != nil {
		envs, err = dnsProvider.GetAcmeShEnvVars()
		if err != nil {
			return err.Error(), err
		}
	}

	ret, err := shExec(args, envs)
	if err != nil {
		return ret, err
	}

	return "", nil
}

// shExec executes the acme.sh with arguments
func shExec(args []string, envs []string) (string, error) {
	acmeSh, err := getAcmeShFilePath()
	if err != nil {
		logger.Error("AcmeShError", err)
		return "", err
	}

	logger.Debug("CMD: %s %v", acmeSh, args)
	// nolint: gosec
	c := exec.Command(acmeSh, args...)
	c.Env = append(getCommonEnvVars(), envs...)

	b, e := c.Output()

	if e != nil {
		logger.Error("AcmeShError", fmt.Errorf("Command error: %s -- %v\n%+v", acmeSh, args, e))
		logger.Warn(string(b))
	}

	return string(b), e
}

func getCommonArgs() []string {
	args := make([]string, 0)

	if config.Configuration.Acmesh.Home != "" {
		args = append(args, "--home", config.Configuration.Acmesh.Home)
	}
	if config.Configuration.Acmesh.ConfigHome != "" {
		args = append(args, "--config-home", config.Configuration.Acmesh.ConfigHome)
	}
	if config.Configuration.Acmesh.CertHome != "" {
		args = append(args, "--cert-home", config.Configuration.Acmesh.CertHome)
	}

	args = append(args, "--log", "/data/logs/acme.sh.log")
	args = append(args, "--debug", "2")

	return args
}

// This is split out into it's own function so it's testable
func buildCertRequestArgs(domains []string, method, outputFullchainFile, outputKeyFile string, dnsProvider *dnsprovider.Model, ca *certificateauthority.Model, force bool) ([]string, error) {
	// The argument order matters.
	// see https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert#3-multiple-domains-san-mode--hybrid-mode
	// for multiple domains and note that the method of validation is required just after the domain arg, each time.

	// TODO log file location configurable
	args := []string{"--issue"}

	if ca != nil {
		args = append(args, "--server", ca.AcmeshServer)
		if ca.CABundle != "" {
			args = append(args, "--ca-bundle", ca.CABundle)
		}
	}

	if outputFullchainFile != "" {
		args = append(args, "--fullchain-file", outputFullchainFile)
	}

	if outputKeyFile != "" {
		args = append(args, "--key-file", outputKeyFile)
	}

	methodArgs := make([]string, 0)
	switch method {
	case "dns":
		if dnsProvider == nil {
			return nil, ErrDNSNeedsDNSProvider
		}
		methodArgs = append(methodArgs, "--dns", dnsProvider.AcmeshName)
		if dnsProvider.DNSSleep > 0 {
			// See: https://github.com/acmesh-official/acme.sh/wiki/dnscheck
			methodArgs = append(methodArgs, "--dnssleep", fmt.Sprintf("%d", dnsProvider.DNSSleep))
		}

	case "http":
		if dnsProvider != nil {
			return nil, ErrHTTPHasDNSProvider
		}
		methodArgs = append(methodArgs, "-w", config.Configuration.Acmesh.GetWellknown())
	default:
		return nil, ErrMethodNotSupported
	}

	hasMethod := false

	// Add domains to args
	for _, domain := range domains {
		args = append(args, "-d", domain)
		// Method has to appear after each domain
		if !hasMethod {
			args = append(args, methodArgs...)
			hasMethod = true
		}
	}

	if force {
		args = append(args, "--force")
	}

	args = append(args, getCommonArgs()...)

	return args, nil
}
