package keys

import (
	"encoding/json"
	"fmt"
	"os"

	"github.com/open-policy-agent/opa/v1/util"
)

const defaultSigningAlgorithm = "RS256"

var supportedAlgos = map[string]struct{}{
	"ES256": {}, "ES384": {}, "ES512": {},
	"HS256": {}, "HS384": {}, "HS512": {},
	"PS256": {}, "PS384": {}, "PS512": {},
	"RS256": {}, "RS384": {}, "RS512": {},
}

// IsSupportedAlgorithm true if provided alg is supported
func IsSupportedAlgorithm(alg string) bool {
	_, ok := supportedAlgos[alg]
	return ok
}

// Config holds the keys used to sign or verify bundles and tokens
type Config struct {
	Key        string `json:"key"`
	PrivateKey string `json:"private_key"`
	Algorithm  string `json:"algorithm"`
	Scope      string `json:"scope"`
}

// Equal returns true if this key config is equal to the other.
func (k *Config) Equal(other *Config) bool {
	if k == other {
		return true
	}
	return other != nil && *k == *other
}

func (k *Config) validateAndInjectDefaults(id string) error {
	if k.Key == "" && k.PrivateKey == "" {
		return fmt.Errorf("invalid keys configuration: no keys provided for key ID %v", id)
	}

	if k.Algorithm == "" {
		k.Algorithm = defaultSigningAlgorithm
	}

	if !IsSupportedAlgorithm(k.Algorithm) {
		return fmt.Errorf("unsupported algorithm '%v'", k.Algorithm)
	}

	return nil
}

// NewKeyConfig return a new Config
func NewKeyConfig(key, alg, scope string) (*Config, error) {
	var pubKey string
	if _, err := os.Stat(key); err == nil {
		bs, err := os.ReadFile(key)
		if err != nil {
			return nil, err
		}
		pubKey = string(bs)
	} else if os.IsNotExist(err) {
		pubKey = key
	} else {
		return nil, err
	}

	return &Config{
		Key:       pubKey,
		Algorithm: alg,
		Scope:     scope,
	}, nil
}

// ParseKeysConfig returns a map containing the key and the signing algorithm
func ParseKeysConfig(raw json.RawMessage) (map[string]*Config, error) {
	keys := map[string]*Config{}
	var obj map[string]json.RawMessage

	if err := util.Unmarshal(raw, &obj); err == nil {
		for k := range obj {
			var keyConfig Config
			if err = util.Unmarshal(obj[k], &keyConfig); err != nil {
				return nil, err
			}

			if err = keyConfig.validateAndInjectDefaults(k); err != nil {
				return nil, err
			}

			keys[k] = &keyConfig
		}
	} else {
		return nil, err
	}
	return keys, nil
}
