package pgp

import (
	"errors"
	"os/exec"
	"regexp"
)

// GPGVersion stores discovered GPG version
type GPGVersion int

// GPG version as discovered
const (
	GPG1x      GPGVersion = 1
	GPG20x     GPGVersion = 2
	GPG21x     GPGVersion = 3
	GPG22xPlus GPGVersion = 4
)

// GPGFinder implement search for gpg executables and returns version of discovered executables
type GPGFinder interface {
	FindGPG() (gpg string, version GPGVersion, err error)
	FindGPGV() (gpgv string, version GPGVersion, err error)
}

type pathGPGFinder struct {
	gpgNames     []string
	gpgvNames    []string
	errorMessage string

	expectedVersionSubstring string
}

type iteratingGPGFinder struct {
	finders      []GPGFinder
	errorMessage string
}

// GPGDefaultFinder looks for GPG1 first, but falls back to GPG2 if GPG1 is not available
func GPGDefaultFinder() GPGFinder {
	return &iteratingGPGFinder{
		finders:      []GPGFinder{GPG1Finder(), GPG2Finder()},
		errorMessage: "Couldn't find a suitable gpg executable. Make sure gnupg is installed",
	}
}

// GPG1Finder looks for GnuPG1.x only
func GPG1Finder() GPGFinder {
	return &pathGPGFinder{
		gpgNames:                 []string{"gpg", "gpg1"},
		gpgvNames:                []string{"gpgv", "gpgv1"},
		expectedVersionSubstring: `\(GnuPG.*\) (1).(\d)`,
		errorMessage:             "Couldn't find a suitable gpg executable. Make sure gnupg1 is available as either gpg(v) or gpg(v)1 in $PATH",
	}
}

// GPG2Finder looks for GnuPG2.x only
func GPG2Finder() GPGFinder {
	return &pathGPGFinder{
		gpgNames:                 []string{"gpg", "gpg2"},
		gpgvNames:                []string{"gpgv", "gpgv2"},
		expectedVersionSubstring: `\(GnuPG.*\) (2).(\d)`,
		errorMessage:             "Couldn't find a suitable gpg executable. Make sure gnupg2 is available as either gpg(v) or gpg(v)2 in $PATH",
	}
}

func (pgf *pathGPGFinder) FindGPG() (gpg string, version GPGVersion, err error) {
	for _, cmd := range pgf.gpgNames {
		var result bool
		result, version = cliVersionCheck(cmd, pgf.expectedVersionSubstring)
		if result {
			gpg = cmd
			break
		}
	}

	if gpg == "" {
		err = errors.New(pgf.errorMessage)
	}

	return
}

func (pgf *pathGPGFinder) FindGPGV() (gpgv string, version GPGVersion, err error) {
	for _, cmd := range pgf.gpgvNames {
		var result bool
		result, version = cliVersionCheck(cmd, pgf.expectedVersionSubstring)
		if result {
			gpgv = cmd
			break
		}
	}

	if gpgv == "" {
		err = errors.New(pgf.errorMessage)
	}

	return
}

func (it *iteratingGPGFinder) FindGPG() (gpg string, version GPGVersion, err error) {
	for _, finder := range it.finders {
		gpg, version, err = finder.FindGPG()
		if err == nil {
			return
		}
	}

	err = errors.New(it.errorMessage)

	return
}

func (it *iteratingGPGFinder) FindGPGV() (gpg string, version GPGVersion, err error) {
	for _, finder := range it.finders {
		gpg, version, err = finder.FindGPGV()
		if err == nil {
			return
		}
	}

	err = errors.New(it.errorMessage)

	return
}

func cliVersionCheck(cmd string, marker string) (result bool, version GPGVersion) {
	output, err := exec.Command(cmd, "--version").CombinedOutput()
	if err != nil {
		return
	}

	strOutput := string(output)
	regex := regexp.MustCompile(marker)

	version = GPG22xPlus
	matches := regex.FindStringSubmatch(strOutput)
	result = (matches != nil)
	if matches != nil {
		if matches[1] == "1" {
			version = GPG1x
		} else if matches[1] == "2" && matches[2] == "0" {
			version = GPG20x
		} else if matches[1] == "2" && matches[2] == "1" {
			version = GPG21x
		}
	}

	return
}
