package securitypolicy

import (
	"crypto/sha256"
	_ "embed"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"regexp"
	"strconv"
	"strings"
	"syscall"

	"github.com/Microsoft/hcsshim/internal/guestpath"
	"github.com/opencontainers/runtime-spec/specs-go"
	"github.com/pkg/errors"
)

//go:embed framework.rego
var frameworkCodeTemplate string

//go:embed api.rego
var apiCodeTemplate string

var APICode = strings.Replace(apiCodeTemplate, "@@API_VERSION@@", apiVersion, 1)
var FrameworkCode = strings.Replace(frameworkCodeTemplate, "@@FRAMEWORK_VERSION@@", frameworkVersion, 1)

var ErrInvalidOpenDoorPolicy = errors.New("allow_all cannot be set to 'true' when Containers are non-empty")

type EnvVarRule string

const (
	EnvVarRuleString EnvVarRule = "string"
	EnvVarRuleRegex  EnvVarRule = "re2"
)

type IDNameStrategy string

const (
	IDNameStrategyName  IDNameStrategy = "name"
	IDNameStrategyID    IDNameStrategy = "id"
	IDNameStrategyRegex IDNameStrategy = "re2"
	IDNameStrategyAny   IDNameStrategy = "any"
)

const plan9Prefix = "plan9://"

const (
	SecurityContextDirTemplate = "security-context-*"
	PolicyFilename             = "security-policy-base64"
	HostAMDCertFilename        = "host-amd-cert-base64"
	ReferenceInfoFilename      = "reference-info-base64"
)

// PolicyConfig contains toml or JSON config for security policy.
type PolicyConfig struct {
	AllowAll                         bool                    `json:"allow_all" toml:"allow_all"`
	Containers                       []ContainerConfig       `json:"containers" toml:"container"`
	ExternalProcesses                []ExternalProcessConfig `json:"external_processes" toml:"external_process"`
	Fragments                        []FragmentConfig        `json:"fragments" toml:"fragment"`
	AllowPropertiesAccess            bool                    `json:"allow_properties_access" toml:"allow_properties_access"`
	AllowDumpStacks                  bool                    `json:"allow_dump_stacks" toml:"allow_dump_stacks"`
	AllowRuntimeLogging              bool                    `json:"allow_runtime_logging" toml:"allow_runtime_logging"`
	AllowEnvironmentVariableDropping bool                    `json:"allow_environment_variable_dropping" toml:"allow_environment_variable_dropping"`
	// AllowUnencryptedScratch is a global policy configuration that allows
	// all containers within a pod to be run without scratch encryption.
	AllowUnencryptedScratch bool `json:"allow_unencrypted_scratch" toml:"allow_unencrypted_scratch"`
	AllowCapabilityDropping bool `json:"allow_capability_dropping" toml:"allow_capability_dropping"`
}

func NewPolicyConfig(opts ...PolicyConfigOpt) (*PolicyConfig, error) {
	p := &PolicyConfig{}
	for _, o := range opts {
		if err := o(p); err != nil {
			return nil, err
		}
	}
	return p, nil
}

// ExternalProcessConfig contains toml or JSON config for running external processes in the UVM.
type ExternalProcessConfig struct {
	Command          []string `json:"command" toml:"command"`
	WorkingDir       string   `json:"working_dir" toml:"working_dir"`
	AllowStdioAccess bool     `json:"allow_stdio_access" toml:"allow_stdio_access"`
}

// FragmentConfig contains toml or JSON config for including elements from fragments.
type FragmentConfig struct {
	Issuer     string   `json:"issuer" toml:"issuer"`
	Feed       string   `json:"feed" toml:"feed"`
	MinimumSVN string   `json:"minimum_svn" toml:"minimum_svn"`
	Includes   []string `json:"includes" toml:"include"`
}

// AuthConfig contains toml or JSON config for registry authentication.
type AuthConfig struct {
	Username string `json:"username" toml:"username"`
	Password string `json:"password" toml:"password"`
}

// EnvRuleConfig contains toml or JSON config for environment variable
// security policy enforcement.
type EnvRuleConfig struct {
	Strategy EnvVarRule `json:"strategy" toml:"strategy"`
	Rule     string     `json:"rule" toml:"rule"`
	Required bool       `json:"required" toml:"required"`
}

type IDNameConfig struct {
	Strategy IDNameStrategy `json:"strategy" toml:"strategy"`
	Rule     string         `json:"rule" toml:"rule"`
}

type UserConfig struct {
	UserIDName   IDNameConfig   `json:"user_idname" toml:"user_idname"`
	GroupIDNames []IDNameConfig `json:"group_idnames" toml:"group_idname"`
	Umask        string         `json:"umask" toml:"umask"`
}

type IDName struct {
	ID   string
	Name string
}

func MeasureSeccompProfile(seccomp *specs.LinuxSeccomp) (string, error) {
	if seccomp == nil {
		return "", nil
	}

	buf, err := json.Marshal(seccomp)
	if err != nil {
		return "", err
	}

	profileSHA256 := sha256.Sum256(buf)
	return fmt.Sprintf("%x", profileSHA256), nil
}

const policyDecisionPattern = `policyDecision< %s >policyDecision`

func ExtractPolicyDecision(errorMessage string) (string, error) {
	re := regexp.MustCompile(fmt.Sprintf(policyDecisionPattern, `(.*)`))
	matches := re.FindStringSubmatch(errorMessage)
	if len(matches) != 2 {
		return "", errors.Errorf("unable to extract policy decision from error message: %s", errorMessage)
	}

	errorBytes, err := base64.StdEncoding.DecodeString(matches[1])
	if err != nil {
		return "", err
	}

	return string(errorBytes), nil
}

// ContainerConfig contains toml or JSON config for container described
// in security policy.
type ContainerConfig struct {
	ImageName                string              `json:"image_name" toml:"image_name"`
	Command                  []string            `json:"command" toml:"command"`
	Auth                     AuthConfig          `json:"auth" toml:"auth"`
	EnvRules                 []EnvRuleConfig     `json:"env_rules" toml:"env_rule"`
	WorkingDir               string              `json:"working_dir" toml:"working_dir"`
	Mounts                   []MountConfig       `json:"mounts" toml:"mount"`
	AllowElevated            bool                `json:"allow_elevated" toml:"allow_elevated"`
	ExecProcesses            []ExecProcessConfig `json:"exec_processes" toml:"exec_process"`
	Signals                  []syscall.Signal    `json:"signals" toml:"signals"`
	AllowStdioAccess         bool                `json:"allow_stdio_access" toml:"allow_stdio_access"`
	AllowPrivilegeEscalation bool                `json:"allow_privilege_escalation" toml:"allow_privilege_escalation"`
	User                     *UserConfig         `json:"user" toml:"user"`
	Capabilities             *CapabilitiesConfig `json:"capabilities" toml:"capabilities"`
	SeccompProfilePath       string              `json:"seccomp_profile_path" toml:"seccomp_profile_path"`
}

// MountConfig contains toml or JSON config for mount security policy
// constraint description.
type MountConfig struct {
	HostPath      string `json:"host_path" toml:"host_path"`
	ContainerPath string `json:"container_path" toml:"container_path"`
	Readonly      bool   `json:"readonly" toml:"readonly"`
}

// ExecProcessConfig contains toml or JSON config for exec process security
// policy constraint description
type ExecProcessConfig struct {
	Command []string         `json:"command" toml:"command"`
	Signals []syscall.Signal `json:"signals" toml:"signals"`
}

// CapabilitiesConfig contains the toml or JSON config for capabilies security
// polict constraint description
type CapabilitiesConfig struct {
	Bounding    []string `json:"bounding" toml:"bounding"`
	Effective   []string `json:"effective" toml:"effective"`
	Inheritable []string `json:"inheritable" toml:"inheritable"`
	Permitted   []string `json:"permitted" toml:"permitted"`
	Ambient     []string `json:"ambient" toml:"ambient"`
}

//go:embed version_api
var apiVersion string

//go:embed version_framework
var frameworkVersion string

// NewEnvVarRules creates slice of EnvRuleConfig's from environment variables
// strings slice.
func NewEnvVarRules(envVars []string, required bool) []EnvRuleConfig {
	var rules []EnvRuleConfig
	for _, env := range envVars {
		r := EnvRuleConfig{
			Strategy: EnvVarRuleString,
			Rule:     env,
			Required: required,
		}
		rules = append(rules, r)
	}
	return rules
}

// NewOpenDoorPolicy creates a new SecurityPolicy with AllowAll set to `true`
func NewOpenDoorPolicy() *SecurityPolicy {
	return &SecurityPolicy{
		AllowAll: true,
	}
}

// NewSecurityPolicyDigest decodes base64 encoded policy string, computes
// and returns sha256 digest
func NewSecurityPolicyDigest(base64policy string) ([]byte, error) {
	jsonPolicy, err := base64.StdEncoding.DecodeString(base64policy)
	if err != nil {
		return nil, fmt.Errorf("failed to decode base64 security policy: %w", err)
	}
	digest := sha256.New()
	digest.Write(jsonPolicy)
	digestBytes := digest.Sum(nil)
	return digestBytes, nil
}

// EncodedSecurityPolicy is a JSON representation of SecurityPolicy that has
// been base64 encoded for storage in an annotation embedded within another
// JSON configuration
type EncodedSecurityPolicy struct {
	SecurityPolicy string `json:"SecurityPolicy,omitempty"`
}

type SecurityPolicy struct {
	// Flag that when set to true allows for all checks to pass. Currently, used
	// to run with security policy enforcement "running dark"; checks can be in
	// place but the default policy that is created on startup has AllowAll set
	// to true, thus making policy enforcement effectively "off" from a logical
	// standpoint. Policy enforcement isn't actually off as the policy is "allow
	// everything".
	AllowAll bool `json:"allow_all"`
	// One or more containers that are allowed to run
	Containers Containers `json:"containers"`
}

// EncodeToString returns base64 encoded string representation of SecurityPolicy.
func (sp *SecurityPolicy) EncodeToString() (string, error) {
	jsn, err := json.Marshal(sp)
	if err != nil {
		return "", err
	}
	return base64.StdEncoding.EncodeToString(jsn), nil
}

type Containers struct {
	Length   int                  `json:"length"`
	Elements map[string]Container `json:"elements"`
}

type Container struct {
	Command              CommandArgs         `json:"command"`
	EnvRules             EnvRules            `json:"env_rules"`
	Layers               Layers              `json:"layers"`
	WorkingDir           string              `json:"working_dir"`
	Mounts               Mounts              `json:"mounts"`
	AllowElevated        bool                `json:"allow_elevated"`
	ExecProcesses        []ExecProcessConfig `json:"-"`
	Signals              []syscall.Signal    `json:"-"`
	AllowStdioAccess     bool                `json:"-"`
	NoNewPrivileges      bool                `json:"-"`
	User                 UserConfig          `json:"-"`
	Capabilities         *CapabilitiesConfig `json:"-"`
	SeccompProfileSHA256 string              `json:"-"`
}

// StringArrayMap wraps an array of strings as a string map.
type StringArrayMap struct {
	Length   int               `json:"length"`
	Elements map[string]string `json:"elements"`
}

type Layers StringArrayMap

type CommandArgs StringArrayMap

type Options StringArrayMap

type EnvRules struct {
	Length   int                      `json:"length"`
	Elements map[string]EnvRuleConfig `json:"elements"`
}

type Mount struct {
	Source      string  `json:"source"`
	Destination string  `json:"destination"`
	Type        string  `json:"type"`
	Options     Options `json:"options"`
}

type Mounts struct {
	Length   int              `json:"length"`
	Elements map[string]Mount `json:"elements"`
}

// CreateContainerPolicy creates a new Container policy instance from the
// provided constraints or an error if parameter validation fails.
func CreateContainerPolicy(
	command, layers []string,
	envRules []EnvRuleConfig,
	workingDir string,
	mounts []MountConfig,
	allowElevated bool,
	execProcesses []ExecProcessConfig,
	signals []syscall.Signal,
	allowStdioAccess bool,
	noNewPrivileges bool,
	user UserConfig,
	capabilities *CapabilitiesConfig,
	seccompProfileSHA256 string,
) (*Container, error) {
	if err := validateEnvRules(envRules); err != nil {
		return nil, err
	}
	if err := validateMountConstraint(mounts); err != nil {
		return nil, err
	}
	return &Container{
		Command:              newCommandArgs(command),
		Layers:               newLayers(layers),
		EnvRules:             newEnvRules(envRules),
		WorkingDir:           workingDir,
		Mounts:               newMountConstraints(mounts),
		AllowElevated:        allowElevated,
		ExecProcesses:        execProcesses,
		Signals:              signals,
		AllowStdioAccess:     allowStdioAccess,
		NoNewPrivileges:      noNewPrivileges,
		User:                 user,
		Capabilities:         capabilities,
		SeccompProfileSHA256: seccompProfileSHA256,
	}, nil
}

// NewSecurityPolicy creates a new SecurityPolicy from the provided values.
func NewSecurityPolicy(allowAll bool, containers []*Container) *SecurityPolicy {
	containersMap := map[string]Container{}
	for i, c := range containers {
		containersMap[strconv.Itoa(i)] = *c
	}
	return &SecurityPolicy{
		AllowAll: allowAll,
		Containers: Containers{
			Elements: containersMap,
		},
	}
}

func validateEnvRules(rules []EnvRuleConfig) error {
	for _, rule := range rules {
		switch rule.Strategy {
		case EnvVarRuleRegex:
			if _, err := regexp.Compile(rule.Rule); err != nil {
				return err
			}
		}
	}
	return nil
}

func validateMountConstraint(mounts []MountConfig) error {
	for _, m := range mounts {
		if _, err := regexp.Compile(m.HostPath); err != nil {
			return err
		}
	}
	return nil
}

func newCommandArgs(args []string) CommandArgs {
	command := map[string]string{}
	for i, arg := range args {
		command[strconv.Itoa(i)] = arg
	}
	return CommandArgs{
		Elements: command,
	}
}

func newEnvRules(rs []EnvRuleConfig) EnvRules {
	envRules := map[string]EnvRuleConfig{}
	for i, r := range rs {
		envRules[strconv.Itoa(i)] = r
	}
	return EnvRules{
		Elements: envRules,
	}
}

func newLayers(ls []string) Layers {
	layers := map[string]string{}
	for i, l := range ls {
		layers[strconv.Itoa(i)] = l
	}
	return Layers{
		Elements: layers,
	}
}

func newMountOptions(opts []string) Options {
	mountOpts := map[string]string{}
	for i, o := range opts {
		mountOpts[strconv.Itoa(i)] = o
	}
	return Options{
		Elements: mountOpts,
	}
}

// newOptionsFromConfig applies the same logic as CRI plugin to generate
// mount options given readonly and propagation config.
// TODO: (anmaxvl) update when support for other mount types is added,
// e.g., vhd:// or evd://
// TODO: (anmaxvl) Do we need to set/validate Linux rootfs propagation?
// In case we do, update securityPolicyContainer and Container structs
// as well as mount enforcement logic.
func newOptionsFromConfig(mCfg *MountConfig) []string {
	mountOpts := []string{"rbind"}

	if strings.HasPrefix(mCfg.HostPath, guestpath.SandboxMountPrefix) ||
		strings.HasPrefix(mCfg.HostPath, guestpath.HugePagesMountPrefix) {
		mountOpts = append(mountOpts, "rshared")
	} else {
		mountOpts = append(mountOpts, "rprivate")
	}

	if mCfg.Readonly {
		mountOpts = append(mountOpts, "ro")
	} else {
		mountOpts = append(mountOpts, "rw")
	}
	return mountOpts
}

// newMountTypeFromConfig mimics the behavior in CRI when figuring out OCI
// mount type.
func newMountTypeFromConfig(mCfg *MountConfig) string {
	if strings.HasPrefix(mCfg.HostPath, guestpath.SandboxMountPrefix) ||
		strings.HasPrefix(mCfg.HostPath, guestpath.HugePagesMountPrefix) ||
		strings.HasPrefix(mCfg.HostPath, plan9Prefix) {
		return "bind"
	}
	return "none"
}

// newMountFromConfig converts user provided MountConfig into internal representation
// of mount constraint.
func newMountFromConfig(mCfg *MountConfig) Mount {
	opts := newOptionsFromConfig(mCfg)
	return Mount{
		Source:      mCfg.HostPath,
		Destination: mCfg.ContainerPath,
		Type:        newMountTypeFromConfig(mCfg),
		Options:     newMountOptions(opts),
	}
}

// newMountConstraints creates Mounts from a given array of MountConfig's.
func newMountConstraints(mountConfigs []MountConfig) Mounts {
	mounts := map[string]Mount{}
	for i, mc := range mountConfigs {
		mounts[strconv.Itoa(i)] = newMountFromConfig(&mc)
	}
	return Mounts{
		Elements: mounts,
	}
}

func EmptyCapabiltiesSet() []string {
	return make([]string, 0)
}

func DefaultUnprivilegedCapabilities() []string {
	return []string{"CAP_CHOWN",
		"CAP_DAC_OVERRIDE",
		"CAP_FSETID",
		"CAP_FOWNER",
		"CAP_MKNOD",
		"CAP_NET_RAW",
		"CAP_SETGID",
		"CAP_SETUID",
		"CAP_SETFCAP",
		"CAP_SETPCAP",
		"CAP_NET_BIND_SERVICE",
		"CAP_SYS_CHROOT",
		"CAP_KILL",
		"CAP_AUDIT_WRITE",
	}
}

func DefaultPrivilegedCapabilities() []string {
	return []string{"CAP_CHOWN",
		"CAP_DAC_OVERRIDE",
		"CAP_DAC_READ_SEARCH",
		"CAP_FOWNER",
		"CAP_FSETID",
		"CAP_KILL",
		"CAP_SETGID",
		"CAP_SETUID",
		"CAP_SETPCAP",
		"CAP_LINUX_IMMUTABLE",
		"CAP_NET_BIND_SERVICE",
		"CAP_NET_BROADCAST",
		"CAP_NET_ADMIN",
		"CAP_NET_RAW",
		"CAP_IPC_LOCK",
		"CAP_IPC_OWNER",
		"CAP_SYS_MODULE",
		"CAP_SYS_RAWIO",
		"CAP_SYS_CHROOT",
		"CAP_SYS_PTRACE",
		"CAP_SYS_PACCT",
		"CAP_SYS_ADMIN",
		"CAP_SYS_BOOT",
		"CAP_SYS_NICE",
		"CAP_SYS_RESOURCE",
		"CAP_SYS_TIME",
		"CAP_SYS_TTY_CONFIG",
		"CAP_MKNOD",
		"CAP_LEASE",
		"CAP_AUDIT_WRITE",
		"CAP_AUDIT_CONTROL",
		"CAP_SETFCAP",
		"CAP_MAC_OVERRIDE",
		"CAP_MAC_ADMIN",
		"CAP_SYSLOG",
		"CAP_WAKE_ALARM",
		"CAP_BLOCK_SUSPEND",
		"CAP_AUDIT_READ",
		"CAP_PERFMON",
		"CAP_BPF",
		"CAP_CHECKPOINT_RESTORE",
	}
}
