package gpgbin

import (
	"bufio"
	"bytes"
	"fmt"
	"io"
	"os/exec"
	"strconv"
	"strings"

	"git.sr.ht/~rjarry/aerc/lib/log"
	"git.sr.ht/~rjarry/aerc/lib/pinentry"
	"git.sr.ht/~rjarry/aerc/models"
)

// gpg represents a gpg command with buffers attached to stdout and stderr
type gpg struct {
	cmd    *exec.Cmd
	stdout bytes.Buffer
	stderr bytes.Buffer
}

// newGpg creates a new gpg command with buffers attached
func newGpg(stdin io.Reader, args []string) *gpg {
	g := new(gpg)
	g.cmd = exec.Command("gpg", "--status-fd", "2", "--log-file", "/dev/null", "--batch")
	g.cmd.Args = append(g.cmd.Args, args...)
	g.cmd.Stdin = stdin
	g.cmd.Stdout = &g.stdout
	g.cmd.Stderr = &g.stderr

	pinentry.SetCmdEnv(g.cmd)

	return g
}

// fields returns the field name from --status-fd output. See:
// https://github.com/gpg/gnupg/blob/master/doc/DETAILS
func field(s string) string {
	tokens := strings.SplitN(s, " ", 3)
	if tokens[0] == "[GNUPG:]" {
		return tokens[1]
	}
	return ""
}

// getIdentity returns the identity of the given key
func getIdentity(key uint64) string {
	fpr := fmt.Sprintf("%X", key)
	cmd := exec.Command("gpg", "--with-colons", "--batch", "--list-keys", fpr)

	var outbuf strings.Builder
	cmd.Stdout = &outbuf
	err := cmd.Run()
	if err != nil {
		log.Errorf("gpg: failed to get identity: %v", err)
		return ""
	}
	out := strings.Split(outbuf.String(), "\n")
	for _, line := range out {
		if strings.HasPrefix(line, "uid") {
			flds := strings.Split(line, ":")
			return flds[9]
		}
	}
	return ""
}

// getKeyId returns the 16 digit key id, if key exists
func getKeyId(s string, private bool) string {
	cmd := exec.Command("gpg", "--with-colons", "--batch")
	listArg := "--list-keys"
	if private {
		listArg = "--list-secret-keys"
	}
	cmd.Args = append(cmd.Args, listArg, s)

	var outbuf strings.Builder
	cmd.Stdout = &outbuf
	err := cmd.Run()
	if err != nil {
		log.Errorf("gpg: failed to get key ID: %v", err)
		return ""
	}
	out := strings.Split(outbuf.String(), "\n")
	for _, line := range out {
		if strings.HasPrefix(line, "fpr") {
			flds := strings.Split(line, ":")
			id := flds[9]
			return id[len(id)-16:]
		}
	}
	return ""
}

// longKeyToUint64 returns a uint64 version of the given key
func longKeyToUint64(key string) (uint64, error) {
	fpr := string(key[len(key)-16:])
	fprUint64, err := strconv.ParseUint(fpr, 16, 64)
	if err != nil {
		return 0, err
	}
	return fprUint64, nil
}

// parse parses the output of gpg --status-fd
func parseStatusFd(r io.Reader, md *models.MessageDetails) error {
	var err error
	scanner := bufio.NewScanner(r)
	for scanner.Scan() {
		line := scanner.Text()
		if field(line) == "PLAINTEXT_LENGTH" {
			continue
		}
		log.Tracef(line)

		switch field(line) {
		case "ENC_TO":
			md.IsEncrypted = true
		case "DECRYPTION_KEY":
			md.DecryptedWithKeyId, err = parseDecryptionKey(line)
			md.DecryptedWith = getIdentity(md.DecryptedWithKeyId)
			if err != nil {
				return err
			}
		case "DECRYPTION_FAILED":
			return EncryptionFailed
		case "NEWSIG":
			md.IsSigned = true
		case "GOODSIG":
			t := strings.SplitN(line, " ", 4)
			md.SignedByKeyId, err = longKeyToUint64(t[2])
			if err != nil {
				return err
			}
			md.SignedBy = t[3]
		case "BADSIG":
			t := strings.SplitN(line, " ", 4)
			md.SignedByKeyId, err = longKeyToUint64(t[2])
			if err != nil {
				return err
			}
			md.SignatureError = "gpg: invalid signature"
			md.SignedBy = t[3]
		case "EXPSIG":
			t := strings.SplitN(line, " ", 4)
			md.SignedByKeyId, err = longKeyToUint64(t[2])
			if err != nil {
				return err
			}
			md.SignatureError = "gpg: expired signature"
			md.SignedBy = t[3]
		case "EXPKEYSIG":
			t := strings.SplitN(line, " ", 4)
			md.SignedByKeyId, err = longKeyToUint64(t[2])
			if err != nil {
				return err
			}
			md.SignatureError = "gpg: signature made with expired key"
			md.SignedBy = t[3]
		case "REVKEYSIG":
			t := strings.SplitN(line, " ", 4)
			md.SignedByKeyId, err = longKeyToUint64(t[2])
			if err != nil {
				return err
			}
			md.SignatureError = "gpg: signature made with revoked key"
			md.SignedBy = t[3]
		case "ERRSIG":
			t := strings.SplitN(line, " ", 9)
			md.SignedByKeyId, err = longKeyToUint64(t[2])
			if err != nil {
				return err
			}
			if t[7] == "9" {
				md.SignatureError = "gpg: missing public key"
			}
			if t[7] == "4" {
				md.SignatureError = "gpg: unsupported algorithm"
			}
			md.SignedBy = "(unknown signer)"
		case "INV_RECP":
			t := strings.SplitN(line, " ", 4)
			if t[2] == "10" {
				return fmt.Errorf("gpg: public key of %s is not trusted", t[3])
			}
		case "SIG_CREATED":
			fields := strings.Split(line, " ")
			micalg, err := strconv.Atoi(fields[4])
			if err != nil {
				return MicalgNotFound
			}
			md.Micalg = micalgs[micalg]
		case "VALIDSIG":
			fields := strings.Split(line, " ")
			micalg, err := strconv.Atoi(fields[9])
			if err != nil {
				return MicalgNotFound
			}
			md.Micalg = micalgs[micalg]
		case "NODATA":
			t := strings.SplitN(line, " ", 3)
			if t[2] == "4" {
				md.SignatureError = "gpg: no signature packet found"
			}
			if t[2] == "1" {
				return NoValidOpenPgpData
			}
		case "FAILURE":
			if line == "[GNUPG:] FAILURE gpg-exit 33554433" {
				log.Errorf("skipping gpg failure: gpg-exit 33554433")
				break
			}
			return fmt.Errorf("%s", strings.TrimPrefix(line, "[GNUPG:] "))
		}
	}
	return nil
}

// parseDecryptionKey returns primary key from DECRYPTION_KEY line
func parseDecryptionKey(l string) (uint64, error) {
	key := strings.Split(l, " ")[3]
	fpr := string(key[len(key)-16:])
	fprUint64, err := longKeyToUint64(fpr)
	if err != nil {
		return 0, err
	}
	getIdentity(fprUint64)
	return fprUint64, nil
}

type StatusFdParsingError int32

const (
	EncryptionFailed StatusFdParsingError = iota + 1
	MicalgNotFound
	NoValidOpenPgpData
)

func (err StatusFdParsingError) Error() string {
	switch err {
	case EncryptionFailed:
		return "gpg: decryption failed"
	case MicalgNotFound:
		return "gpg: micalg not found"
	case NoValidOpenPgpData:
		return "gpg: no valid OpenPGP data found"
	default:
		return "gpg: unknown status fd parsing error"
	}
}

// micalgs represent hash algorithms for signatures. These are ignored by many
// email clients, but can be used as an additional verification so are sent.
// Both gpgmail and pgpmail implementations in aerc check for matching micalgs
var micalgs = map[int]string{
	1:  "pgp-md5",
	2:  "pgp-sha1",
	3:  "pgp-ripemd160",
	8:  "pgp-sha256",
	9:  "pgp-sha384",
	10: "pgp-sha512",
	11: "pgp-sha224",
}
