package sandbox

import (
	"fmt"
	"strings"

	"github.com/safedep/dry/utils"
)

// SandboxPolicy represents a parsed and validated sandbox policy that defines
// filesystem, network, and process execution restrictions for package managers.
// Policy violations will block execution. Policy supports inheritance from parent policies.
type SandboxPolicy struct {
	// These fields are not affected by inheritance and are set from the child policy.
	Name            string   `yaml:"name" json:"name"`
	Description     string   `yaml:"description" json:"description"`
	Inherits        string   `yaml:"inherits,omitempty" json:"inherits,omitempty"`
	PackageManagers []string `yaml:"package_managers" json:"package_managers"`

	// These fields are affected by inheritance and are merged with the parent policy.
	// Any new values added here should be handled in the MergeWithParent method.
	Filesystem  FilesystemPolicy  `yaml:"filesystem" json:"filesystem"`
	Network     NetworkPolicy     `yaml:"network" json:"network"`
	Process     ProcessPolicy     `yaml:"process" json:"process"`
	Environment EnvironmentPolicy `yaml:"environment" json:"environment"`

	// The boolean fields are pointers to allow for nil values so that the YAML parser
	// can set the values from the child policy if present. We can differentiate between
	// nil and false values. Any new values added here should be handled in the
	// MergeWithParent method. When present in the child policy, the parent value is ignored.
	// When not present in the child policy, the parent value is used.

	// AllowGitConfig allows write access to .git/config file.
	AllowGitConfig *bool `yaml:"allow_git_config" json:"allow_git_config"`

	// AllowPTY allows pseudo-terminal (PTY) operations.
	AllowPTY *bool `yaml:"allow_pty" json:"allow_pty"`

	// AllowNetworkBind allows binding to localhost (127.0.0.1 / ::1) for listening.
	AllowNetworkBind *bool `yaml:"allow_network_bind" json:"allow_network_bind"`

	// NetworkViaProxyOnly confines all outbound network access to the PMG
	// proxy. Requires the proxy flow; drivers fail closed without a running
	// proxy.
	NetworkViaProxyOnly *bool `yaml:"network_via_proxy_only" json:"network_via_proxy_only"`

	// AllowDirectDNS re-opens direct DNS (mDNSResponder) under
	// NetworkViaProxyOnly. No effect otherwise. Default false: the proxy
	// resolves names and direct DNS is an exfiltration channel.
	AllowDirectDNS *bool `yaml:"allow_direct_dns" json:"allow_direct_dns"`
}

// FilesystemPolicy defines allowed and denied filesystem access patterns.
// Deny rules have higher priority than allow rules.
type FilesystemPolicy struct {
	AllowRead  []string `yaml:"allow_read" json:"allow_read"`
	AllowWrite []string `yaml:"allow_write" json:"allow_write"`
	DenyRead   []string `yaml:"deny_read" json:"deny_read"`
	DenyWrite  []string `yaml:"deny_write" json:"deny_write"`
}

// NetworkPolicy defines allowed and denied network access patterns.
// Patterns are in the format "host:port" or "*:*" for wildcards.
type NetworkPolicy struct {
	AllowOutbound []string `yaml:"allow_outbound" json:"allow_outbound"`
	DenyOutbound  []string `yaml:"deny_outbound" json:"deny_outbound"`
	AllowBind     []string `yaml:"allow_bind" json:"allow_bind"`
}

// ProcessPolicy defines allowed and denied process execution patterns.
// Patterns can be specific paths or glob patterns.
type ProcessPolicy struct {
	AllowExec []string `yaml:"allow_exec" json:"allow_exec"`
	DenyExec  []string `yaml:"deny_exec" json:"deny_exec"`
}

// EnvironmentPolicy controls which environment variables are scrubbed from the
// child process. Deny extends the built-in util.DANGEROUS_ENV_VARS list; Allow
// suppresses matching denies (allow wins). Patterns are case-insensitive name
// globs. Enforcement happens in sandbox/util.ScrubEnv at process spawn.
type EnvironmentPolicy struct {
	Allow []string `yaml:"allow" json:"allow"`
	Deny  []string `yaml:"deny" json:"deny"`
}

// Validate validates the sandbox policy for correctness before inheritance resolution.
// Returns an error if the policy is invalid.
// Note: Validation for "at least one rule" check is deferred to ValidateResolved(),
// since a child policy might have no rules of its own but inherit rules from its parent.
func (p *SandboxPolicy) Validate() error {
	if p.Name == "" {
		return fmt.Errorf("policy name is required")
	}

	if len(p.PackageManagers) == 0 {
		return fmt.Errorf("policy must specify at least one package manager")
	}

	return nil
}

// ValidateResolved validates a policy after inheritance has been resolved.
// This is called after MergeWithParent to ensure the final policy is valid.
func (p *SandboxPolicy) ValidateResolved() error {
	if err := p.Validate(); err != nil {
		return err
	}

	// Check that at least one access rule is defined (after inheritance)
	hasRules := len(p.Filesystem.AllowRead) > 0 ||
		len(p.Filesystem.AllowWrite) > 0 ||
		len(p.Filesystem.DenyRead) > 0 ||
		len(p.Filesystem.DenyWrite) > 0 ||
		len(p.Network.AllowOutbound) > 0 ||
		len(p.Network.DenyOutbound) > 0 ||
		len(p.Process.AllowExec) > 0 ||
		len(p.Process.DenyExec) > 0 ||
		len(p.Environment.Allow) > 0 ||
		len(p.Environment.Deny) > 0

	if !hasRules {
		return fmt.Errorf("policy must define at least one access rule (after inheritance resolution)")
	}

	return nil
}

// AppliesToPackageManager returns true if this policy applies to the given package manager.
func (p *SandboxPolicy) AppliesToPackageManager(pm string) bool {
	pmLower := strings.ToLower(pm)
	for _, supported := range p.PackageManagers {
		if strings.ToLower(supported) == pmLower {
			return true
		}
	}

	return false
}

// MergeWithParent merges the child policy with its parent policy.
// Lists are unioned (additive), package_managers are replaced, booleans are overridden.
// This method modifies the receiver (child policy) in place.
func (child *SandboxPolicy) MergeWithParent(parent *SandboxPolicy) {
	// Union filesystem lists
	child.Filesystem.AllowRead = unionStringSlices(parent.Filesystem.AllowRead, child.Filesystem.AllowRead)
	child.Filesystem.AllowWrite = unionStringSlices(parent.Filesystem.AllowWrite, child.Filesystem.AllowWrite)
	child.Filesystem.DenyRead = unionStringSlices(parent.Filesystem.DenyRead, child.Filesystem.DenyRead)
	child.Filesystem.DenyWrite = unionStringSlices(parent.Filesystem.DenyWrite, child.Filesystem.DenyWrite)

	// Union network lists
	child.Network.AllowOutbound = unionStringSlices(parent.Network.AllowOutbound, child.Network.AllowOutbound)
	child.Network.DenyOutbound = unionStringSlices(parent.Network.DenyOutbound, child.Network.DenyOutbound)
	child.Network.AllowBind = unionStringSlices(parent.Network.AllowBind, child.Network.AllowBind)

	// Union process lists
	child.Process.AllowExec = unionStringSlices(parent.Process.AllowExec, child.Process.AllowExec)
	child.Process.DenyExec = unionStringSlices(parent.Process.DenyExec, child.Process.DenyExec)

	// Union environment lists
	child.Environment.Allow = unionStringSlices(parent.Environment.Allow, child.Environment.Allow)
	child.Environment.Deny = unionStringSlices(parent.Environment.Deny, child.Environment.Deny)

	// Set boolean fields by duplicating the parent value if not present in the child.
	if child.AllowPTY == nil {
		child.AllowPTY = utils.PtrTo(utils.SafelyGetValue(parent.AllowPTY))
	}

	if child.AllowGitConfig == nil {
		child.AllowGitConfig = utils.PtrTo(utils.SafelyGetValue(parent.AllowGitConfig))
	}

	if child.AllowNetworkBind == nil {
		child.AllowNetworkBind = utils.PtrTo(utils.SafelyGetValue(parent.AllowNetworkBind))
	}

	if child.NetworkViaProxyOnly == nil {
		child.NetworkViaProxyOnly = utils.PtrTo(utils.SafelyGetValue(parent.NetworkViaProxyOnly))
	}

	if child.AllowDirectDNS == nil {
		child.AllowDirectDNS = utils.PtrTo(utils.SafelyGetValue(parent.AllowDirectDNS))
	}
}

// unionStringSlices returns a new slice containing all unique elements from both slices.
// Order is preserved: parent entries first, then child entries (excluding duplicates).
func unionStringSlices(parent, child []string) []string {
	seen := make(map[string]bool)
	result := make([]string, 0, len(parent)+len(child))

	// Add all parent entries
	for _, item := range parent {
		if !seen[item] {
			seen[item] = true
			result = append(result, item)
		}
	}

	// Add child entries that aren't duplicates
	for _, item := range child {
		if !seen[item] {
			seen[item] = true
			result = append(result, item)
		}
	}

	return result
}
