package colons

import (
	"bufio"
	"io"
	"regexp"
	"strings"

	"github.com/gopasspw/gopass/internal/backend/crypto/gpg"
)

var (
	// nolint:godot
	// John Doe (user) <john.doe@example.com>
	reUIDComment = regexp.MustCompile(`([^(<]+)\s+(\([^)]+\))\s+<([^>]+)>`)
	// nolint:godot
	// John Doe <john.doe@example.com>
	reUID = regexp.MustCompile(`([^(<]+)\s+<([^>]+)>`)
	// nolint:godot
	// John Doe (user)
	reUIDNoEmailComment = regexp.MustCompile(`([^(<]+)\s+(\([^)]+\))`)
)

// http://git.gnupg.org/cgi-bin/gitweb.cgi?p=gnupg.git;a=blob_plain;f=doc/DETAILS
// Fields:
// 0 - Type of record
//     Types:
//     pub - Public Key
//     crt - X.509 cert
//     crs - X.509 cert and private key
//     sub - Subkey (Secondary Key)
//     sec - Secret / Private Key
//     ssb - Secret Subkey
//     uid - User ID
//     uat - User attribute
//     sig - Signature
//     rev - Revocation Signature
//     fpr - Fingerprint (field 9)
//     pkd - Public Key Data
//     grp - Keygrip
//     rvk - Revocation KEy
//     tfs - TOFU stats
//     tru - Trust database info
//     spk - Signature subpacket
//     cfg - Configuration data
// 1 - Validity
// 2 - Key length
// 3 - Public Key Algo
// 4 - KeyID
// 5 - Creation Date (UTC)
// 6 - Expiration Date
// 7 - Cert S/N
// 8 - Ownertrust
// 9 - User-ID
// 10 - Sign. Class
// 11 - Key Caps.
// 12 - Issuer cert fp
// 13 - Flag
// 14 - S/N of a token
// 15 - Hash algo (2 - SHA-1, 8 - SHA-256)
// 16 - Curve Name

// Parse parses the `--with-colons` output format of GPG.
func Parse(reader io.Reader) gpg.KeyList {
	kl := make(gpg.KeyList, 0, 100)

	scanner := bufio.NewScanner(reader)

	var cur gpg.Key

	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())
		fields := strings.Split(line, ":")

		switch fields[0] {
		case "pub":
			fallthrough
		case "sec":
			if cur.Fingerprint != "" && cur.KeyLength > 0 {
				kl = append(kl, cur)
			}

			validity := fields[1]
			if validity == "" && fields[0] == "sec" {
				validity = "u"
			}

			cur = gpg.Key{
				KeyType:        fields[0],
				Validity:       validity,
				KeyLength:      parseInt(fields[2]),
				CreationDate:   parseTS(fields[5]),
				ExpirationDate: parseTS(fields[6]),
				Ownertrust:     fields[8],
				Identities:     make(map[string]gpg.Identity, 1),
				SubKeys:        make(map[string]struct{}, 1),
				Caps:           parseKeyCaps(fields[11]),
			}
		case "sub":
			fallthrough
		case "ssb":
			cur.SubKeys[fields[4]] = struct{}{}
		case "fpr":
			if cur.Fingerprint == "" {
				cur.Fingerprint = fields[9]
			}
		case "uid":
			sn := fields[7]
			cur.Identities[sn] = parseColonIdentity(fields)
		}
	}

	if cur.Fingerprint != "" && cur.KeyLength > 0 {
		kl = append(kl, cur)
	}

	return kl
}

func parseKeyCaps(field string) gpg.Capabilities {
	keycaps := gpg.Capabilities{}

	if strings.Contains(field, "S") {
		keycaps.Sign = true
	}

	if strings.Contains(field, "E") {
		keycaps.Encrypt = true
	}

	if strings.Contains(field, "C") {
		keycaps.Certify = true
	}

	if strings.Contains(field, "A") {
		keycaps.Authentication = true
	}

	if strings.Contains(field, "D") {
		keycaps.Deactivated = true
	}

	return keycaps
}

func parseColonIdentity(fields []string) gpg.Identity {
	for i, f := range fields {
		fields[i] = strings.ReplaceAll(f, "\\x3a", ":")
	}

	id := fields[9]
	ni := gpg.Identity{
		Name:           id,
		CreationDate:   parseTS(fields[5]),
		ExpirationDate: parseTS(fields[6]),
	}

	if reUIDComment.MatchString(id) {
		if m := reUIDComment.FindStringSubmatch(id); len(m) > 3 {
			ni.Name = m[1]
			ni.Comment = strings.Trim(m[2], "()")
			ni.Email = m[3]

			return ni
		}
	}

	if reUIDNoEmailComment.MatchString(id) {
		if m := reUIDNoEmailComment.FindStringSubmatch(id); len(m) > 2 {
			ni.Name = m[1]
			ni.Comment = strings.Trim(m[2], "()")
		}

		return ni
	}

	if reUID.MatchString(id) {
		if m := reUID.FindStringSubmatch(id); len(m) > 2 {
			ni.Name = m[1]
			ni.Email = m[2]

			return ni
		}
	}

	return ni
}
