/*
Package config provides a way to find and load SOPS configuration files
*/
package config //import "github.com/getsops/sops/v3/config"

import (
	"fmt"
	"os"
	"path"
	"path/filepath"
	"regexp"
	"strings"

	"github.com/getsops/sops/v3"
	"github.com/getsops/sops/v3/age"
	"github.com/getsops/sops/v3/azkv"
	"github.com/getsops/sops/v3/gcpkms"
	"github.com/getsops/sops/v3/hckms"
	"github.com/getsops/sops/v3/hcvault"
	"github.com/getsops/sops/v3/kms"
	"github.com/getsops/sops/v3/pgp"
	"github.com/getsops/sops/v3/publish"
	"go.yaml.in/yaml/v3"
)

type fileSystem interface {
	Stat(name string) (os.FileInfo, error)
}

type osFS struct {
	stat func(string) (os.FileInfo, error)
}

func (fs osFS) Stat(name string) (os.FileInfo, error) {
	return fs.stat(name)
}

var fs fileSystem = osFS{stat: os.Stat}

const (
	maxDepth            = 100
	configFileName      = ".sops.yaml"
	alternateConfigName = ".sops.yml"
)

// ConfigFileResult contains the path to a config file and any warnings
type ConfigFileResult struct {
	Path    string
	Warning string
}

// LookupConfigFile looks for a sops config file in the current working directory
// and on parent directories, up to the maxDepth limit.
// It returns a result containing the file path and any warnings.
func LookupConfigFile(start string) (ConfigFileResult, error) {
	filepath := path.Dir(start)
	var foundAlternatePath string

	for i := 0; i < maxDepth; i++ {
		configPath := path.Join(filepath, configFileName)
		_, err := fs.Stat(configPath)
		if err == nil {
			result := ConfigFileResult{Path: configPath}

			if foundAlternatePath != "" {
				result.Warning = fmt.Sprintf(
					"ignoring %q when searching for config file; the config file must be called %q; using %q instead",
					foundAlternatePath, configFileName, configPath)
			}
			return result, nil
		}

		// Check for alternate filename if we haven't found one yet
		if foundAlternatePath == "" {
			alternatePath := path.Join(filepath, alternateConfigName)
			_, altErr := fs.Stat(alternatePath)
			if altErr == nil {
				foundAlternatePath = alternatePath
			}
		}

		filepath = path.Join(filepath, "..")
	}

	// No config file found
	result := ConfigFileResult{}
	if foundAlternatePath != "" {
		result.Warning = fmt.Sprintf(
			"ignoring %q when searching for config file; the config file must be called %q",
			foundAlternatePath, configFileName)
	}

	return result, fmt.Errorf("config file not found")
}

// FindConfigFile looks for a sops config file in the current working directory and on parent directories, up to the limit defined by the maxDepth constant.
func FindConfigFile(start string) (string, error) {
	result, err := LookupConfigFile(start)
	return result.Path, err
}

type DotenvStoreConfig struct{}

type INIStoreConfig struct{}

type JSONStoreConfig struct {
	Indent int `yaml:"indent"`
}

type JSONBinaryStoreConfig struct {
	Indent int `yaml:"indent"`
}

type YAMLStoreConfig struct {
	Indent int `yaml:"indent"`
}

type StoresConfig struct {
	Dotenv     DotenvStoreConfig     `yaml:"dotenv"`
	INI        INIStoreConfig        `yaml:"ini"`
	JSONBinary JSONBinaryStoreConfig `yaml:"json_binary"`
	JSON       JSONStoreConfig       `yaml:"json"`
	YAML       YAMLStoreConfig       `yaml:"yaml"`
}

type configFile struct {
	CreationRules    []creationRule    `yaml:"creation_rules"`
	DestinationRules []destinationRule `yaml:"destination_rules"`
	Stores           StoresConfig      `yaml:"stores"`
}

type keyGroup struct {
	Merge   []keyGroup   `yaml:"merge"`
	KMS     []kmsKey     `yaml:"kms"`
	GCPKMS  []gcpKmsKey  `yaml:"gcp_kms"`
	HCKms   []hckmsKey   `yaml:"hckms"`
	AzureKV []azureKVKey `yaml:"azure_keyvault"`
	Vault   []string     `yaml:"hc_vault"`
	Age     []string     `yaml:"age"`
	PGP     []string     `yaml:"pgp"`
}

type gcpKmsKey struct {
	ResourceID string `yaml:"resource_id"`
}

type kmsKey struct {
	Arn        string             `yaml:"arn"`
	Role       string             `yaml:"role,omitempty"`
	Context    map[string]*string `yaml:"context"`
	AwsProfile string             `yaml:"aws_profile"`
}

type azureKVKey struct {
	VaultURL string `yaml:"vaultUrl"`
	Key      string `yaml:"key"`
	Version  string `yaml:"version"`
}

type hckmsKey struct {
	KeyID string `yaml:"key_id"`
}

type destinationRule struct {
	PathRegex        string       `yaml:"path_regex"`
	S3Bucket         string       `yaml:"s3_bucket"`
	S3Prefix         string       `yaml:"s3_prefix"`
	GCSBucket        string       `yaml:"gcs_bucket"`
	GCSPrefix        string       `yaml:"gcs_prefix"`
	VaultPath        string       `yaml:"vault_path"`
	VaultAddress     string       `yaml:"vault_address"`
	VaultKVMountName string       `yaml:"vault_kv_mount_name"`
	VaultKVVersion   int          `yaml:"vault_kv_version"`
	RecreationRule   creationRule `yaml:"recreation_rule,omitempty"`
	OmitExtensions   bool         `yaml:"omit_extensions"`
}

type creationRule struct {
	PathRegex               string      `yaml:"path_regex"`
	KMS                     interface{} `yaml:"kms"` // string or []string
	AwsProfile              string      `yaml:"aws_profile"`
	Age                     interface{} `yaml:"age"`     // string or []string
	PGP                     interface{} `yaml:"pgp"`     // string or []string
	GCPKMS                  interface{} `yaml:"gcp_kms"` // string or []string
	HCKms                   []string    `yaml:"hckms"`
	AzureKeyVault           interface{} `yaml:"azure_keyvault"`       // string or []string
	VaultURI                interface{} `yaml:"hc_vault_transit_uri"` // string or []string
	KeyGroups               []keyGroup  `yaml:"key_groups"`
	ShamirThreshold         int         `yaml:"shamir_threshold"`
	UnencryptedSuffix       string      `yaml:"unencrypted_suffix"`
	EncryptedSuffix         string      `yaml:"encrypted_suffix"`
	UnencryptedRegex        string      `yaml:"unencrypted_regex"`
	EncryptedRegex          string      `yaml:"encrypted_regex"`
	UnencryptedCommentRegex string      `yaml:"unencrypted_comment_regex"`
	EncryptedCommentRegex   string      `yaml:"encrypted_comment_regex"`
	MACOnlyEncrypted        bool        `yaml:"mac_only_encrypted"`
}

// Helper methods to safely extract keys as []string
func (c *creationRule) GetKMSKeys() ([]string, error) {
	return parseKeyField(c.KMS, "kms")
}

func (c *creationRule) GetAgeKeys() ([]string, error) {
	return parseKeyField(c.Age, "age")
}

func (c *creationRule) GetPGPKeys() ([]string, error) {
	return parseKeyField(c.PGP, "pgp")
}

func (c *creationRule) GetGCPKMSKeys() ([]string, error) {
	return parseKeyField(c.GCPKMS, "gcp_kms")
}

func (c *creationRule) GetAzureKeyVaultKeys() ([]string, error) {
	return parseKeyField(c.AzureKeyVault, "azure_keyvault")
}

func (c *creationRule) GetVaultURIs() ([]string, error) {
	return parseKeyField(c.VaultURI, "hc_vault_transit_uri")
}

// Utility function to handle both string and []string
func parseKeyField(field interface{}, fieldName string) ([]string, error) {
	if field == nil {
		return []string{}, nil
	}

	switch v := field.(type) {
	case string:
		if v == "" {
			return []string{}, nil
		}
		// Existing CSV parsing logic
		keys := strings.Split(v, ",")
		result := make([]string, 0, len(keys))
		for _, key := range keys {
			trimmed := strings.TrimSpace(key)
			if trimmed != "" { // Skip empty strings (fixes trailing comma issue)
				result = append(result, trimmed)
			}
		}
		return result, nil
	case []interface{}:
		result := make([]string, len(v))
		for i, item := range v {
			if str, ok := item.(string); ok {
				result[i] = str
			} else {
				return nil, fmt.Errorf("invalid %s key configuration: expected string in list, got %T", fieldName, item)
			}
		}
		return result, nil
	case []string:
		return v, nil
	default:
		return nil, fmt.Errorf("invalid %s key configuration: expected string, []string, or nil, got %T", fieldName, field)
	}
}

func NewStoresConfig() *StoresConfig {
	storesConfig := &StoresConfig{}
	storesConfig.JSON.Indent = -1
	storesConfig.JSONBinary.Indent = -1
	return storesConfig
}

// Load loads a sops config file into a temporary struct
func (f *configFile) load(bytes []byte) error {
	err := yaml.Unmarshal(bytes, f)
	if err != nil {
		return fmt.Errorf("Could not unmarshal config file: %s", err)
	}
	return nil
}

// Config is the configuration for a given SOPS file
type Config struct {
	KeyGroups               []sops.KeyGroup
	ShamirThreshold         int
	UnencryptedSuffix       string
	EncryptedSuffix         string
	UnencryptedRegex        string
	EncryptedRegex          string
	UnencryptedCommentRegex string
	EncryptedCommentRegex   string
	MACOnlyEncrypted        bool
	Destination             publish.Destination
	OmitExtensions          bool
}

func deduplicateKeygroup(group sops.KeyGroup) sops.KeyGroup {
	var deduplicatedKeygroup sops.KeyGroup

	unique := make(map[string]bool)
	for _, v := range group {
		key := fmt.Sprintf("%T/%v", v, v.ToString())
		if _, ok := unique[key]; ok {
			// key already contained, therefore not unique
			continue
		}

		deduplicatedKeygroup = append(deduplicatedKeygroup, v)
		unique[key] = true
	}

	return deduplicatedKeygroup
}

func extractMasterKeys(group keyGroup) (sops.KeyGroup, error) {
	var keyGroup sops.KeyGroup
	for _, k := range group.Merge {
		subKeyGroup, err := extractMasterKeys(k)
		if err != nil {
			return nil, err
		}
		keyGroup = append(keyGroup, subKeyGroup...)
	}

	for _, k := range group.Age {
		keys, err := age.MasterKeysFromRecipients(k)
		if err != nil {
			return nil, err
		}
		for _, key := range keys {
			keyGroup = append(keyGroup, key)
		}
	}
	for _, k := range group.PGP {
		keyGroup = append(keyGroup, pgp.NewMasterKeyFromFingerprint(k))
	}
	for _, k := range group.KMS {
		keyGroup = append(keyGroup, kms.NewMasterKeyWithProfile(k.Arn, k.Role, k.Context, k.AwsProfile))
	}
	for _, k := range group.GCPKMS {
		keyGroup = append(keyGroup, gcpkms.NewMasterKeyFromResourceID(k.ResourceID))
	}
	for _, k := range group.HCKms {
		key, err := hckms.NewMasterKey(k.KeyID)
		if err != nil {
			return nil, err
		}
		keyGroup = append(keyGroup, key)
	}
	for _, k := range group.AzureKV {
		if key, err := azkv.NewMasterKeyWithOptionalVersion(k.VaultURL, k.Key, k.Version); err == nil {
			keyGroup = append(keyGroup, key)
		} else {
			return nil, err
		}
	}
	for _, k := range group.Vault {
		if masterKey, err := hcvault.NewMasterKeyFromURI(k); err == nil {
			keyGroup = append(keyGroup, masterKey)
		} else {
			return nil, err
		}
	}
	return deduplicateKeygroup(keyGroup), nil
}

func getKeysWithValidation(getKeysFunc func() ([]string, error), keyType string) ([]string, error) {
	keys, err := getKeysFunc()
	if err != nil {
		return nil, fmt.Errorf("invalid %s key configuration: %w", keyType, err)
	}
	return keys, nil
}

func getKeyGroupsFromCreationRule(cRule *creationRule, kmsEncryptionContext map[string]*string) ([]sops.KeyGroup, error) {
	var groups []sops.KeyGroup
	if len(cRule.KeyGroups) > 0 {
		for _, group := range cRule.KeyGroups {
			keyGroup, err := extractMasterKeys(group)
			if err != nil {
				return nil, err
			}
			groups = append(groups, keyGroup)
		}
	} else {
		var keyGroup sops.KeyGroup
		ageKeys, err := getKeysWithValidation(cRule.GetAgeKeys, "age")
		if err != nil {
			return nil, err
		}

		if len(ageKeys) > 0 {
			ageKeys, err := age.MasterKeysFromRecipients(strings.Join(ageKeys, ","))
			if err != nil {
				return nil, err
			} else {
				for _, ak := range ageKeys {
					keyGroup = append(keyGroup, ak)
				}
			}
		}
		pgpKeys, err := getKeysWithValidation(cRule.GetPGPKeys, "pgp")
		if err != nil {
			return nil, err
		}
		for _, k := range pgp.MasterKeysFromFingerprintString(strings.Join(pgpKeys, ",")) {
			keyGroup = append(keyGroup, k)
		}
		kmsKeys, err := getKeysWithValidation(cRule.GetKMSKeys, "kms")
		if err != nil {
			return nil, err
		}
		for _, k := range kms.MasterKeysFromArnString(strings.Join(kmsKeys, ","), kmsEncryptionContext, cRule.AwsProfile) {
			keyGroup = append(keyGroup, k)
		}
		gcpkmsKeys, err := getKeysWithValidation(cRule.GetGCPKMSKeys, "gcpkms")
		if err != nil {
			return nil, err
		}
		for _, k := range gcpkms.MasterKeysFromResourceIDString(strings.Join(gcpkmsKeys, ",")) {
			keyGroup = append(keyGroup, k)
		}
		hckmsMasterKeys, err := hckms.NewMasterKeyFromKeyIDString(strings.Join(cRule.HCKms, ","))
		if err != nil {
			return nil, err
		}
		for _, k := range hckmsMasterKeys {
			keyGroup = append(keyGroup, k)
		}
		azKeys, err := getKeysWithValidation(cRule.GetAzureKeyVaultKeys, "azure_keyvault")
		if err != nil {
			return nil, err
		}
		azureKeys, err := azkv.MasterKeysFromURLs(strings.Join(azKeys, ","))
		if err != nil {
			return nil, err
		}
		for _, k := range azureKeys {
			keyGroup = append(keyGroup, k)
		}
		vaultKeyUris, err := getKeysWithValidation(cRule.GetVaultURIs, "vault")
		if err != nil {
			return nil, err
		}
		vaultKeys, err := hcvault.NewMasterKeysFromURIs(strings.Join(vaultKeyUris, ","))
		if err != nil {
			return nil, err
		}
		for _, k := range vaultKeys {
			keyGroup = append(keyGroup, k)
		}
		groups = append(groups, keyGroup)
	}
	return groups, nil
}

func loadConfigFile(confPath string) (*configFile, error) {
	confBytes, err := os.ReadFile(confPath)
	if err != nil {
		return nil, fmt.Errorf("could not read config file: %s", err)
	}
	conf := &configFile{}
	conf.Stores = *NewStoresConfig()
	err = conf.load(confBytes)
	if err != nil {
		return nil, fmt.Errorf("error loading config: %s", err)
	}
	return conf, nil
}

func configFromRule(rule *creationRule, kmsEncryptionContext map[string]*string) (*Config, error) {
	cryptRuleCount := 0
	if rule.UnencryptedSuffix != "" {
		cryptRuleCount++
	}
	if rule.EncryptedSuffix != "" {
		cryptRuleCount++
	}
	if rule.UnencryptedRegex != "" {
		cryptRuleCount++
	}
	if rule.EncryptedRegex != "" {
		cryptRuleCount++
	}
	if rule.UnencryptedCommentRegex != "" {
		cryptRuleCount++
	}
	if rule.EncryptedCommentRegex != "" {
		cryptRuleCount++
	}

	if cryptRuleCount > 1 {
		return nil, fmt.Errorf("error loading config: cannot use more than one of encrypted_suffix, unencrypted_suffix, encrypted_regex, unencrypted_regex, encrypted_comment_regex, or unencrypted_comment_regex for the same rule")
	}

	groups, err := getKeyGroupsFromCreationRule(rule, kmsEncryptionContext)
	if err != nil {
		return nil, err
	}

	return &Config{
		KeyGroups:               groups,
		ShamirThreshold:         rule.ShamirThreshold,
		UnencryptedSuffix:       rule.UnencryptedSuffix,
		EncryptedSuffix:         rule.EncryptedSuffix,
		UnencryptedRegex:        rule.UnencryptedRegex,
		EncryptedRegex:          rule.EncryptedRegex,
		UnencryptedCommentRegex: rule.UnencryptedCommentRegex,
		EncryptedCommentRegex:   rule.EncryptedCommentRegex,
		MACOnlyEncrypted:        rule.MACOnlyEncrypted,
	}, nil
}

func parseDestinationRuleForFile(conf *configFile, filePath string, kmsEncryptionContext map[string]*string) (*Config, error) {
	var rule *creationRule
	var dRule *destinationRule

	if len(conf.DestinationRules) > 0 {
		for _, r := range conf.DestinationRules {
			if r.PathRegex == "" {
				dRule = &r
				rule = &dRule.RecreationRule
				break
			}
			if r.PathRegex != "" {
				if match, _ := regexp.MatchString(r.PathRegex, filePath); match {
					dRule = &r
					rule = &dRule.RecreationRule
					break
				}
			}
		}
	}

	if dRule == nil {
		return nil, fmt.Errorf("error loading config: no matching destination found in config")
	}

	var dest publish.Destination
	destinationCount := 0
	if dRule.S3Bucket != "" {
		destinationCount++
	}
	if dRule.GCSBucket != "" {
		destinationCount++
	}
	if dRule.VaultPath != "" {
		destinationCount++
	}

	if destinationCount > 1 {
		return nil, fmt.Errorf("error loading config: more than one destinations were found in a single destination rule, you can only use one per rule")
	}
	if dRule.S3Bucket != "" {
		dest = publish.NewS3Destination(dRule.S3Bucket, dRule.S3Prefix)
	}
	if dRule.GCSBucket != "" {
		dest = publish.NewGCSDestination(dRule.GCSBucket, dRule.GCSPrefix)
	}
	if dRule.VaultPath != "" {
		dest = publish.NewVaultDestination(dRule.VaultAddress, dRule.VaultPath, dRule.VaultKVMountName, dRule.VaultKVVersion)
	}

	config, err := configFromRule(rule, kmsEncryptionContext)
	if err != nil {
		return nil, err
	}
	config.Destination = dest
	config.OmitExtensions = dRule.OmitExtensions

	return config, nil
}

func parseCreationRuleForFile(conf *configFile, confPath, filePath string, kmsEncryptionContext map[string]*string) (*Config, error) {
	// If config file doesn't contain CreationRules (it's empty or only contains DestionationRules), assume it does not exist
	if conf.CreationRules == nil {
		return nil, nil
	}

	configDir, err := filepath.Abs(filepath.Dir(confPath))
	if err != nil {
		return nil, err
	}

	// compare file path relative to path of config file
	filePath = strings.TrimPrefix(filePath, configDir+string(filepath.Separator))

	var rule *creationRule

	for _, r := range conf.CreationRules {
		if r.PathRegex == "" {
			rule = &r
			break
		}
		reg, err := regexp.Compile(r.PathRegex)
		if err != nil {
			return nil, fmt.Errorf("can not compile regexp: %w", err)
		}
		if reg.MatchString(filePath) {
			rule = &r
			break
		}
	}

	if rule == nil {
		return nil, fmt.Errorf("error loading config: no matching creation rules found")
	}

	config, err := configFromRule(rule, kmsEncryptionContext)
	if err != nil {
		return nil, err
	}

	return config, nil
}

// LoadCreationRuleForFile load the configuration for a given SOPS file from the config file at confPath. A kmsEncryptionContext
// should be provided for configurations that do not contain key groups, as there's no way to specify context inside
// a SOPS config file outside of key groups.
func LoadCreationRuleForFile(confPath string, filePath string, kmsEncryptionContext map[string]*string) (*Config, error) {
	conf, err := loadConfigFile(confPath)
	if err != nil {
		return nil, err
	}

	return parseCreationRuleForFile(conf, confPath, filePath, kmsEncryptionContext)
}

// LoadDestinationRuleForFile works the same as LoadCreationRuleForFile, but gets the "creation_rule" from the matching destination_rule's
// "recreation_rule".
func LoadDestinationRuleForFile(confPath string, filePath string, kmsEncryptionContext map[string]*string) (*Config, error) {
	conf, err := loadConfigFile(confPath)
	if err != nil {
		return nil, err
	}
	return parseDestinationRuleForFile(conf, filePath, kmsEncryptionContext)
}

func LoadStoresConfig(confPath string) (*StoresConfig, error) {
	conf, err := loadConfigFile(confPath)
	if err != nil {
		return nil, err
	}
	return &conf.Stores, nil
}
