package aws

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base32"
	"encoding/binary"
	"encoding/hex"
	"fmt"
	"strings"

	regexp "github.com/wasilibs/go-re2"

	"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
)

// ResourceTypes derived from: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html#identifiers-unique-ids
var ResourceTypes = map[string]string{
	"ABIA": "AWS STS service bearer token",
	"ACCA": "Context-specific credential",
	"AGPA": "User group",
	"AIDA": "IAM user",
	"AIPA": "Amazon EC2 instance profile",
	"AKIA": "Access key",
	"ANPA": "Managed policy",
	"ANVA": "Version in a managed policy",
	"APKA": "Public key",
	"AROA": "Role",
	"ASCA": "Certificate",
	"ASIA": "Temporary (AWS STS) access key IDs",
}

// UrlEncodedReplacer helps capture base64-encoded results that may be url-encoded.
// TODO: Add this as a decoder, or make it a more generic.
var UrlEncodedReplacer = strings.NewReplacer(
	"%2B", "+",
	"%2b", "+",
	"%2F", "/",
	"%2f", "/",
	"%3d", "=",
	"%3D", "=",
)

// Hashes, like those for git, do technically match the secret pattern.
// But they are extremely unlikely to be generated as an actual AWS secret.
// So when we find them, if they're not verified, we should ignore the result.
var FalsePositiveSecretPat = regexp.MustCompile(`[a-f0-9]{40}`)

func GetAccountNumFromID(id string) (string, error) {
	// Function to get the account number from an AWS ID (no verification required)
	// Source: https://medium.com/@TalBeerySec/a-short-note-on-aws-key-id-f88cc4317489
	if len(id) < 4 {
		return "", fmt.Errorf("AWSID is too short")
	}
	if id[4] == 'I' || id[4] == 'J' {
		return "", fmt.Errorf("can't get account number from AKIAJ/ASIAJ or AKIAI/ASIAI keys")
	}
	trimmedAWSID := id[4:]
	decodedBytes, err := base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(strings.ToUpper(trimmedAWSID))
	if err != nil {
		return "", err
	}

	if len(decodedBytes) < 6 {
		return "", fmt.Errorf("decoded AWSID is too short")
	}

	data := make([]byte, 8)
	copy(data[2:], decodedBytes[0:6])
	z := binary.BigEndian.Uint64(data)
	const mask uint64 = 0x7fffffffff80
	accountNum := (z & mask) >> 7
	return fmt.Sprintf("%012d", accountNum), nil
}

func GetHash(input string) string {
	data := []byte(input)
	hasher := sha256.New()
	hasher.Write(data)
	return hex.EncodeToString(hasher.Sum(nil))
}

func GetHMAC(key []byte, data []byte) []byte {
	hasher := hmac.New(sha256.New, key)
	hasher.Write(data)
	return hasher.Sum(nil)
}

func CleanResults(results []detectors.Result) []detectors.Result {
	if len(results) == 0 {
		return results
	}

	// For every ID, we want at most one result, preferably verified.
	idResults := map[string]detectors.Result{}
	for _, result := range results {
		// Always accept the verified result as the result for the given ID.
		if result.Verified {
			idResults[result.Redacted] = result
			continue
		}

		// Only include an unverified result if we don't already have a result for a given ID.
		if _, exist := idResults[result.Redacted]; !exist {
			idResults[result.Redacted] = result
		}
	}

	var out []detectors.Result
	for _, r := range idResults {
		out = append(out, r)
	}
	return out
}
