// Package decoding implements the configuration side of the upgraded gzip
// decompression framework. The original work only enabled gzip decoding for
// a few endpoints-- here we enable if for all of OPA. Additionally, we provide
// some new defensive configuration options: max_length, and gzip.max_length.
// These allow rejecting requests that indicate their contents are larger than
// the size limits.
//
// The request handling pipeline now looks roughly like this:
//
// Request -> MaxBytesReader(Config.MaxLength) -> ir.CopyN(dest, req, Gzip.MaxLength)
//
// The intent behind this design is to improve how OPA handles large and/or
// malicious requests, compressed or otherwise. The benefit of being a little
// more strict in what we allow is that we can now use "riskier", but
// dramatically more performant techniques, like preallocating content buffers
// for gzipped data. This also should help OPAs in limited memory situations.
package decoding

import (
	"errors"

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

var (
	defaultMaxRequestLength     = int64(268435456) // 256 MB
	defaultGzipMaxContentLength = int64(536870912) // 512 MB
)

// Config represents the configuration for the Server.Decoding settings
type Config struct {
	MaxLength *int64 `json:"max_length,omitempty"` // maximum request size that will be read, regardless of compression.
	Gzip      *Gzip  `json:"gzip,omitempty"`
}

// Gzip represents the configuration for the Server.Decoding.Gzip settings
type Gzip struct {
	MaxLength *int64 `json:"max_length,omitempty"` // Max number of bytes allowed to be read from the decompressor.
}

// ConfigBuilder assists in the construction of the plugin configuration.
type ConfigBuilder struct {
	raw []byte
}

// NewConfigBuilder returns a new ConfigBuilder to build and parse the server config
func NewConfigBuilder() *ConfigBuilder {
	return &ConfigBuilder{}
}

// WithBytes sets the raw server config
func (b *ConfigBuilder) WithBytes(config []byte) *ConfigBuilder {
	b.raw = config
	return b
}

// Parse returns a valid Config object with defaults injected.
func (b *ConfigBuilder) Parse() (*Config, error) {
	if b.raw == nil {
		defaultConfig := &Config{
			MaxLength: &defaultMaxRequestLength,
			Gzip: &Gzip{
				MaxLength: &defaultGzipMaxContentLength,
			},
		}
		return defaultConfig, nil
	}

	var result Config

	if err := util.Unmarshal(b.raw, &result); err != nil {
		return nil, err
	}

	return &result, result.validateAndInjectDefaults()
}

// validateAndInjectDefaults populates defaults if the fields are nil, then
// validates the config values.
func (c *Config) validateAndInjectDefaults() error {
	if c.MaxLength == nil {
		c.MaxLength = &defaultMaxRequestLength
	}

	if c.Gzip == nil {
		c.Gzip = &Gzip{
			MaxLength: &defaultGzipMaxContentLength,
		}
	}
	if c.Gzip.MaxLength == nil {
		c.Gzip.MaxLength = &defaultGzipMaxContentLength
	}

	if *c.MaxLength <= 0 {
		return errors.New("invalid value for server.decoding.max_length field, should be a positive number")
	}
	if *c.Gzip.MaxLength <= 0 {
		return errors.New("invalid value for server.decoding.gzip.max_length field, should be a positive number")
	}

	return nil
}
