// Copyright 2026 Cisco Systems, Inc. and its affiliates
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// SPDX-License-Identifier: Apache-2.0

// Package pfctl generates macOS pfctl anchor rules from a DefenseClaw firewall config.
// No privileged operations — compile only. Applying rules is the administrator's job.
package pfctl

import (
	"fmt"
	"net"
	"regexp"
	"strings"

	"github.com/defenseclaw/defenseclaw/internal/firewall"
)

const anchorName = firewall.DefaultAnchorName

// Compiler generates pfctl rules for macOS.
type Compiler struct{}

func New() *Compiler { return &Compiler{} }

func (c *Compiler) Platform() string { return "pfctl" }

var safeArgPattern = regexp.MustCompile(`^[a-zA-Z0-9._:/%\-]+$`)

func (c *Compiler) ValidateArg(arg string) error {
	if strings.ContainsAny(arg, ";|&`$(){}[]<>!~*?'\"\\") {
		return fmt.Errorf("pfctl: argument contains shell metacharacters: %q", arg)
	}
	if !safeArgPattern.MatchString(arg) {
		return fmt.Errorf("pfctl: argument contains unsafe characters: %q", arg)
	}
	return nil
}

func (c *Compiler) ApplyCommand(rulesPath string) string {
	return fmt.Sprintf("sudo pfctl -a %s -f %s && sudo pfctl -e", anchorName, rulesPath)
}

func (c *Compiler) RemoveCommand() string {
	return fmt.Sprintf("sudo pfctl -a %s -F all", anchorName)
}

// Compile generates pfctl anchor rules from the configuration.
func (c *Compiler) Compile(cfg *firewall.FirewallConfig) ([]string, error) {
	var rules []string

	rules = append(rules,
		"# DefenseClaw egress firewall rules — generated by defenseclaw firewall generate",
		fmt.Sprintf("# default_action: %s", cfg.DefaultAction),
		"",
	)

	// Explicit named rules first.
	for _, rule := range cfg.Rules {
		r, err := c.compileRule(rule)
		if err != nil {
			return nil, fmt.Errorf("pfctl: compile rule %q: %w", rule.Name, err)
		}
		rules = append(rules, fmt.Sprintf("# Rule: %s", rule.Name), r)
	}

	// Allowlist: IPs.
	if len(cfg.Allowlist.IPs) > 0 {
		rules = append(rules, "", "# Allowlist: IPs")
		for _, ip := range cfg.Allowlist.IPs {
			rules = append(rules, c.allowIPRules(ip, cfg.Allowlist.Ports)...)
		}
	}

	// Allowlist: domains (resolved at generation time).
	if len(cfg.Allowlist.Domains) > 0 {
		rules = append(rules, "", "# Allowlist: domains")
		for _, domain := range cfg.Allowlist.Domains {
			if strings.HasPrefix(domain, "*.") {
				rules = append(rules, fmt.Sprintf("# Wildcard domain (resolve manually): %s", domain))
				continue
			}
			ips, err := net.LookupHost(domain)
			if err != nil {
				rules = append(rules, fmt.Sprintf("# Failed to resolve: %s (%v)", domain, err))
				continue
			}
			for _, ip := range ips {
				rules = append(rules, c.allowIPRules(ip, cfg.Allowlist.Ports)...)
				rules[len(rules)-1] += fmt.Sprintf(" # %s", domain)
			}
		}
	}

	// Always allow: established connections, DNS, loopback.
	rules = append(rules,
		"",
		"# Allow established connections",
		"pass out quick inet proto tcp flags A/A",
		"pass out quick inet proto udp keep state",
		"",
		"# Allow DNS",
		"pass out quick inet proto udp to any port 53",
		"pass out quick inet proto tcp to any port 53",
		"",
		"# Allow loopback",
		"pass out quick on lo0 all",
		"",
	)

	// Default action.
	if cfg.DefaultAction == "deny" {
		if cfg.Logging.Enabled {
			rules = append(rules, "# Default: deny with logging", "block out log all")
		} else {
			rules = append(rules, "# Default: deny", "block out all")
		}
	} else {
		rules = append(rules, "# Default: allow", "pass out all")
	}

	return rules, nil
}

func (c *Compiler) allowIPRules(ip string, ports []int) []string {
	proto := "inet"
	if strings.Contains(ip, ":") {
		proto = "inet6"
	}
	if len(ports) == 0 {
		return []string{fmt.Sprintf("pass out quick %s proto tcp to %s", proto, ip)}
	}
	var rules []string
	for _, port := range ports {
		rules = append(rules, fmt.Sprintf("pass out quick %s proto tcp to %s port %d", proto, ip, port))
	}
	return rules
}

func (c *Compiler) compileRule(rule firewall.Rule) (string, error) {
	action := "pass"
	if rule.Action == "deny" {
		action = "block"
	}

	proto := "tcp"
	if rule.Protocol != "" && rule.Protocol != "any" {
		proto = rule.Protocol
	}

	parts := []string{action, "out", "quick"}

	if rule.Destination != "" {
		if err := c.ValidateArg(rule.Destination); err != nil {
			return "", err
		}
		af := "inet"
		if strings.Contains(rule.Destination, ":") {
			af = "inet6"
		}
		parts = append(parts, af, "proto", proto, "to", rule.Destination)
	} else {
		parts = append(parts, "inet", "proto", proto)
	}

	if rule.Port > 0 {
		parts = append(parts, "port", fmt.Sprintf("%d", rule.Port))
	} else if rule.PortRange != "" {
		if err := c.ValidateArg(rule.PortRange); err != nil {
			return "", err
		}
		parts = append(parts, "port", rule.PortRange)
	}

	return strings.Join(parts, " "), nil
}
