package azureappconfigconnectionstring

import (
	"context"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"fmt"
	"net/http"
	"strings"
	"time"

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

	"github.com/trufflesecurity/trufflehog/v3/pkg/common"
	"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
	"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detector_typepb"
)

type Scanner struct {
	client *http.Client
	detectors.DefaultMultiPartCredentialProvider
}

// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)

var (
	defaultClient       = common.SaneHttpClient()
	connectionStringPat = regexp.MustCompile(`Endpoint=(https:\/\/[a-zA-Z0-9-]+\.azconfig\.io);Id=([a-zA-Z0-9+\/=]+);Secret=([a-zA-Z0-9+\/=]+)`)
)

// Keywords are used for efficiently pre-filtering chunks.
// Use identifiers in the secret preferably, or the provider name.
func (s Scanner) Keywords() []string {
	return []string{".azconfig.io"}
}

// FromData will find and optionally verify Azure Management API keys in a given set of bytes.
func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) {
	dataStr := string(data)

	keyMatchesUnique := make(map[string][]string)
	for _, keyMatch := range connectionStringPat.FindAllStringSubmatch(dataStr, -1) {
		keyMatchesUnique[strings.TrimSpace(keyMatch[0])] = keyMatch // keep all the matched groups for verification
	}

	for connectionString, connectionInfo := range keyMatchesUnique {
		endpoint := connectionInfo[1] // Endpoint
		id := connectionInfo[2]       //	Id
		secret := connectionInfo[3]   // Secret
		s1 := detectors.Result{
			DetectorType: detector_typepb.DetectorType_AzureAppConfigConnectionString,
			Raw:          []byte(id),
			RawV2:        []byte(connectionString),
			SecretParts: map[string]string{
				"endpoint": endpoint,
				"id":       id,
				"secret":   secret,
			},
		}

		if verify {
			client := s.client
			if client == nil {
				client = defaultClient
			}

			isVerified, verificationErr := s.verifyMatch(ctx, client, endpoint, id, secret)
			s1.Verified = isVerified

			if verificationErr != nil && !strings.Contains(verificationErr.Error(), "no such host") { // ignore no such host errors
				s1.SetVerificationError(verificationErr, connectionString)
			}
		}

		results = append(results, s1)
	}

	return results, nil
}

func (s Scanner) Type() detector_typepb.DetectorType {
	return detector_typepb.DetectorType_AzureAppConfigConnectionString
}

func (s Scanner) Description() string {
	return "Azure App Configuration is a managed service that centralizes application settings and feature flags, enabling dynamic updates without redeploying applications. Its connection string, which includes the endpoint URL and an access key, securely connects applications to the configuration store."
}

// generateHMACSignature creates the HMAC-SHA256 signature
func generateHMACSignature(secret, stringToSign string) (string, error) {
	decodedSecret, err := base64.StdEncoding.DecodeString(secret)
	if err != nil {
		return "", fmt.Errorf("failed to decode secret: %w", err)
	}

	h := hmac.New(sha256.New, decodedSecret)
	h.Write([]byte(stringToSign))
	signature := base64.StdEncoding.EncodeToString(h.Sum(nil))
	return signature, nil
}

// verifyMatch sends a request to the Azure App Configuration REST API to verify the provided credentials
// https://learn.microsoft.com/en-us/azure/azure-app-configuration/rest-api-authentication-hmac
func (s Scanner) verifyMatch(ctx context.Context, client *http.Client, endpoint, id, secret string) (bool, error) {
	apiVersion := "1.0"
	requestPath := "/kv"
	query := fmt.Sprintf("?api-version=%s", apiVersion)
	url := fmt.Sprintf("%s%s%s", endpoint, requestPath, query)

	// Prepare request
	req, err := http.NewRequest(http.MethodGet, url, nil)
	if err != nil {
		return false, fmt.Errorf("failed to create request: %w", err)
	}

	// Set required headers
	host := strings.TrimPrefix(strings.TrimPrefix(endpoint, "https://"), "http://")
	date := time.Now().UTC().Format(http.TimeFormat)
	contentHash := base64.StdEncoding.EncodeToString(sha256.New().Sum(nil)) // SHA256 hash of an empty body

	req.Header.Set("Host", host)
	req.Header.Set("Date", date)
	req.Header.Set("x-ms-content-sha256", contentHash)

	// Create the string to sign
	stringToSign := fmt.Sprintf("%s\n%s%s\n%s;%s;%s",
		http.MethodGet,
		requestPath,
		query,
		date,
		host,
		contentHash,
	)

	// Generate the HMAC signature
	signature, err := generateHMACSignature(secret, stringToSign)
	if err != nil {
		return false, fmt.Errorf("failed to generate HMAC signature: %w", err)
	}

	// Set the Authorization header
	authorizationHeader := fmt.Sprintf(
		"HMAC-SHA256 Credential=%s&SignedHeaders=date;host;x-ms-content-sha256&Signature=%s",
		id,
		signature,
	)
	req.Header.Set("Authorization", authorizationHeader)

	// Send the request
	resp, err := client.Do(req)
	if err != nil {
		return false, fmt.Errorf("request failed: %w", err)
	}
	defer func() { _ = resp.Body.Close() }()

	// Check the response status
	switch resp.StatusCode {
	case http.StatusOK:
		return true, nil
	case http.StatusUnauthorized:
		return false, nil
	default:
		return false, fmt.Errorf("got unexpected status code: %d", resp.StatusCode)
	}
}
