// Copyright 2018 The mkcert Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package internal

import (
	"bytes"
	"crypto/sha1"
	"crypto/sha256"
	"crypto/x509"
	"encoding/hex"
	"hash"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"strings"
)

var (
	hasJava    bool
	hasKeytool bool

	javaHome    string
	cacertsPath string
	keytoolPath string
	storePass   string = "changeit"
)

func init() {
	if runtime.GOOS == "windows" {
		keytoolPath = filepath.Join("bin", "keytool.exe")
	} else {
		keytoolPath = filepath.Join("bin", "keytool")
	}

	if v := os.Getenv("JAVA_HOME"); v != "" {
		hasJava = true
		javaHome = v

		if pathExists(filepath.Join(v, keytoolPath)) {
			hasKeytool = true
			keytoolPath = filepath.Join(v, keytoolPath)
		}

		if pathExists(filepath.Join(v, "lib", "security", "cacerts")) {
			cacertsPath = filepath.Join(v, "lib", "security", "cacerts")
		}

		if pathExists(filepath.Join(v, "jre", "lib", "security", "cacerts")) {
			cacertsPath = filepath.Join(v, "jre", "lib", "security", "cacerts")
		}
	}
}

func (m *mkcert) checkJava() bool {
	if !hasKeytool {
		return false
	}

	// exists returns true if the given x509.Certificate's fingerprint
	// is in the keytool -list output
	exists := func(c *x509.Certificate, h hash.Hash, keytoolOutput []byte) bool {
		h.Write(c.Raw)
		fp := strings.ToUpper(hex.EncodeToString(h.Sum(nil)))
		return bytes.Contains(keytoolOutput, []byte(fp))
	}

	keytoolOutput, err := exec.Command(keytoolPath, "-list", "-keystore", cacertsPath, "-storepass", storePass).CombinedOutput()
	fatalIfCmdErr(err, "keytool -list", keytoolOutput)
	// keytool outputs SHA1 and SHA256 (Java 9+) certificates in uppercase hex
	// with each octet pair delimitated by ":". Drop them from the keytool output
	keytoolOutput = bytes.Replace(keytoolOutput, []byte(":"), nil, -1)

	// pre-Java 9 uses SHA1 fingerprints
	s1, s256 := sha1.New(), sha256.New()
	return exists(m.caCert, s1, keytoolOutput) || exists(m.caCert, s256, keytoolOutput)
}

func (m *mkcert) installJava() {
	args := []string{
		"-importcert", "-noprompt",
		"-keystore", cacertsPath,
		"-storepass", storePass,
		"-file", filepath.Join(m.CAROOT, rootName),
		"-alias", m.caUniqueName(),
	}

	out, err := execKeytool(exec.Command(keytoolPath, args...))
	fatalIfCmdErr(err, "keytool -importcert", out)
}

func (m *mkcert) uninstallJava() {
	args := []string{
		"-delete",
		"-alias", m.caUniqueName(),
		"-keystore", cacertsPath,
		"-storepass", storePass,
	}
	out, err := execKeytool(exec.Command(keytoolPath, args...))
	if bytes.Contains(out, []byte("does not exist")) {
		return // cert didn't exist
	}
	fatalIfCmdErr(err, "keytool -delete", out)
}

// execKeytool will execute a "keytool" command and if needed re-execute
// the command with commandWithSudo to work around file permissions.
func execKeytool(cmd *exec.Cmd) ([]byte, error) {
	out, err := cmd.CombinedOutput()
	if err != nil && bytes.Contains(out, []byte("java.io.FileNotFoundException")) && runtime.GOOS != "windows" {
		origArgs := cmd.Args[1:]
		cmd = commandWithSudo(cmd.Path)
		cmd.Args = append(cmd.Args, origArgs...)
		cmd.Env = []string{
			"JAVA_HOME=" + javaHome,
		}
		out, err = cmd.CombinedOutput()
	}
	return out, err
}
