//go:build windows

package cli

import (
	"fmt"
	"net/netip"
	"runtime"
	"sync"
	"time"
	"unsafe"

	"golang.org/x/sys/windows"
)

// wfpFirewallState holds the state for WFP-based firewall mode enforcement on Windows.
// When firewall mode is active, we add dynamic WFP permit filters for each allowed IP
// on top of a block-all base filter.
type wfpFirewallState struct {
	mu sync.Mutex

	// pendingAdds and pendingRemoves accumulate changes for batched WFP updates.
	pendingAdds    []netip.Addr
	pendingRemoves []netip.Addr

	// batchTimer fires after the accumulation window to flush pending changes.
	batchTimer *time.Timer

	// filterMap tracks WFP filter IDs for each dynamically allowed IP so we can remove them.
	// Maps IP string → WFP filter ID.
	filterMap map[string]uint64

	// permanentFilterMap tracks permit filters for permanent allowlist entries
	// (loopback/private/link-local/listener/upstream IPs). These stay installed
	// across dynamic allowlist flushes.
	permanentFilterMap map[string]uint64

	// blockFilterIDv4 and blockFilterIDv6 are the base block-all filters.
	blockFilterIDv4 uint64
	blockFilterIDv6 uint64

	// engineHandle is the WFP engine handle from the intercept state.
	engineHandle uintptr
}

const (
	// wfpFirewallBatchInterval is the accumulation window for batching WFP filter updates.
	wfpFirewallBatchInterval = 200 * time.Millisecond
)

// firewallFlushPlatform removes all dynamic WFP permit filters on Windows.
// Called on network changes and config reloads before the in-memory allowlist is flushed.
func (p *prog) firewallFlushPlatform() {
	fwState, ok := p.platformFirewallState.(*wfpFirewallState)
	if !ok || fwState == nil {
		return
	}
	fwState.flushAll(p)
}

// shutdownPlatformFirewall removes Windows firewall-mode WFP filters, including
// dynamic permits, permanent permits, and the base block-all filters.
func (p *prog) shutdownPlatformFirewall() {
	fwState, ok := p.platformFirewallState.(*wfpFirewallState)
	if !ok || fwState == nil {
		return
	}
	fwState.shutdown(p)
}

// initPlatformFirewall initializes Windows-specific firewall enforcement (WFP filters).
func (p *prog) initPlatformFirewall() {
	if fwState, ok := p.platformFirewallState.(*wfpFirewallState); ok && fwState != nil {
		fwState.populatePermanentFilters(p)
		fwState.populateFilters(p)
		return
	}

	if !hardIntercept || p.dnsInterceptState == nil {
		p.Info().Msg("Firewall: WFP enforcement requires hard intercept mode")
		return
	}

	state, ok := p.dnsInterceptState.(*wfpState)
	if !ok || state == nil {
		p.Warn().Msg("Firewall: could not access WFP state for firewall enforcement")
		return
	}

	fwState := &wfpFirewallState{
		filterMap:          make(map[string]uint64),
		permanentFilterMap: make(map[string]uint64),
		engineHandle:       state.engineHandle,
	}
	p.platformFirewallState = fwState

	// Install base block-all outbound filters. We add our firewall filters to the
	// SAME sublayer as DNS intercept with carefully chosen weights:
	//   - Existing DNS permits (localhost): weight 10 (highest, evaluated first)
	//   - Firewall IP permits: weight 5 (middle)
	//   - Existing DNS block: weight 1 (blocks non-localhost DNS)
	//   - Firewall block-all: weight 1 (catch-all for non-DNS)
	//
	// WFP evaluates higher weights first within a sublayer. DNS permits at 10
	// always win, ensuring DNS resolution works. Firewall IP permits at 5
	// override the block-all for resolved IPs. The existing DNS block at 1
	// and our block-all at 1 are both catch-alls (DNS block has port 53
	// conditions so it only catches DNS; our block-all has no conditions
	// so it catches everything else).
	if err := p.addWFPFirewallBlockFilters(fwState); err != nil {
		p.Error().Err(err).Msg("Firewall: failed to install WFP block-all filters")
		return
	}

	// Install permits for the permanent allowlist before enabling dynamic updates.
	// Without these, the block-all filters would also block ctrld upstreams,
	// listener/loopback traffic, LAN ranges, and other permanent exceptions.
	fwState.populatePermanentFilters(p)

	// Register batch callback.
	p.allowList.SetOnBatchChange(func(added []netip.Addr, removed []netip.Addr) {
		fwState.mu.Lock()
		defer fwState.mu.Unlock()

		if len(added) > 0 {
			fwState.pendingAdds = append(fwState.pendingAdds, added...)
		}
		if len(removed) > 0 {
			fwState.pendingRemoves = append(fwState.pendingRemoves, removed...)
		}
		fwState.scheduleBatchFlush(p)
	})

	// Register individual change callback.
	p.allowList.SetOnChange(func(ip netip.Addr, isAdded bool) {
		fwState.mu.Lock()
		defer fwState.mu.Unlock()

		if isAdded {
			fwState.pendingAdds = append(fwState.pendingAdds, ip)
		} else {
			fwState.pendingRemoves = append(fwState.pendingRemoves, ip)
		}
		fwState.scheduleBatchFlush(p)
	})

	// DNS responses may have populated the allowlist before platform callbacks
	// were registered. Add permit filters for that snapshot so WFP starts with
	// the same view as the in-memory allowlist.
	fwState.populateFilters(p)

	p.Info().Msg("Firewall: WFP enforcement initialized with block-all base filters")
}

// scheduleBatchFlush starts or resets the batch timer. Must be called with fwState.mu held.
func (s *wfpFirewallState) scheduleBatchFlush(p *prog) {
	if s.batchTimer != nil {
		return
	}
	s.batchTimer = time.AfterFunc(wfpFirewallBatchInterval, func() {
		s.flushBatch(p)
	})
}

// flushBatch applies accumulated WFP filter changes.
func (s *wfpFirewallState) flushBatch(p *prog) {
	s.mu.Lock()
	defer s.mu.Unlock()

	adds := s.pendingAdds
	removes := s.pendingRemoves
	s.pendingAdds = nil
	s.pendingRemoves = nil
	s.batchTimer = nil

	// Collapse add/remove deltas into the current primary allowlist state. This
	// avoids leaving WFP opposite the allowlist when an Add and Remove for the
	// same IP land in one batch window.
	ipsToSync := make(map[netip.Addr]struct{}, len(adds)+len(removes))
	for _, ip := range adds {
		ipsToSync[ip] = struct{}{}
	}
	for _, ip := range removes {
		ipsToSync[ip] = struct{}{}
	}

	for ip := range ipsToSync {
		key := ip.String()
		allowed := p.allowList != nil && p.allowList.Contains(ip)
		if !allowed {
			s.removePermitFilterLocked(p, key)
			continue
		}

		if _, exists := s.filterMap[key]; exists {
			continue // Already has a permit filter.
		}

		filterID, err := p.addWFPFirewallPermitFilter(s, ip)
		if err != nil {
			p.Warn().Err(err).Msgf("Firewall: failed to add WFP permit filter for %s", key)
			continue
		}
		s.filterMap[key] = filterID
		p.Debug().Msgf("Firewall: added WFP permit filter for %s (ID: %d)", key, filterID)
	}
}

// removePermitFilterLocked removes one dynamic WFP permit filter. s.mu must be held.
func (s *wfpFirewallState) removePermitFilterLocked(p *prog, key string) {
	filterID, ok := s.filterMap[key]
	if !ok {
		return
	}
	r1, _, _ := procFwpmFilterDeleteById0.Call(s.engineHandle, uintptr(filterID))
	if r1 != 0 {
		p.Debug().Msgf("Firewall: failed to remove WFP filter for %s (HRESULT 0x%x, may already be gone)", key, r1)
	} else {
		p.Debug().Msgf("Firewall: removed WFP permit filter for %s", key)
	}
	delete(s.filterMap, key)
}

// flushAll synchronously removes every dynamic WFP permit filter and clears any
// queued batch work. The block-all filters stay installed.
func (s *wfpFirewallState) flushAll(p *prog) {
	s.mu.Lock()
	if s.batchTimer != nil {
		s.batchTimer.Stop()
		s.batchTimer = nil
	}
	s.pendingAdds = nil
	s.pendingRemoves = nil

	filters := make(map[string]uint64, len(s.filterMap))
	for key, filterID := range s.filterMap {
		filters[key] = filterID
	}
	s.filterMap = make(map[string]uint64)
	s.mu.Unlock()

	for key, filterID := range filters {
		r1, _, _ := procFwpmFilterDeleteById0.Call(s.engineHandle, uintptr(filterID))
		if r1 != 0 {
			p.Debug().Msgf("Firewall: failed to remove WFP filter for %s during flush (HRESULT 0x%x, may already be gone)", key, r1)
		} else {
			p.Debug().Msgf("Firewall: removed WFP permit filter for %s during flush", key)
		}
	}
}

// shutdown removes every WFP filter owned by firewall mode.
func (s *wfpFirewallState) shutdown(p *prog) {
	s.flushAll(p)

	s.mu.Lock()
	permanentFilters := make(map[string]uint64, len(s.permanentFilterMap))
	for key, filterID := range s.permanentFilterMap {
		permanentFilters[key] = filterID
	}
	s.permanentFilterMap = make(map[string]uint64)

	blockIDs := []uint64{s.blockFilterIDv4, s.blockFilterIDv6}
	s.blockFilterIDv4 = 0
	s.blockFilterIDv6 = 0
	s.mu.Unlock()

	for key, filterID := range permanentFilters {
		if r1, _, _ := procFwpmFilterDeleteById0.Call(s.engineHandle, uintptr(filterID)); r1 != 0 {
			p.Debug().Msgf("Firewall: failed to remove permanent WFP filter for %s during shutdown (HRESULT 0x%x, may already be gone)", key, r1)
		}
	}
	for _, filterID := range blockIDs {
		if filterID == 0 {
			continue
		}
		if r1, _, _ := procFwpmFilterDeleteById0.Call(s.engineHandle, uintptr(filterID)); r1 != 0 {
			p.Debug().Msgf("Firewall: failed to remove WFP block filter %d during shutdown (HRESULT 0x%x, may already be gone)", filterID, r1)
		}
	}
}

// populatePermanentFilters mirrors the in-memory permanent allowlist into WFP
// permit filters so the base block-all rule does not block ctrld itself, local
// network traffic, or upstream resolver endpoints.
func (s *wfpFirewallState) populatePermanentFilters(p *prog) {
	if p.allowList == nil {
		return
	}
	addrs, prefixes := p.allowList.PermanentEntries()

	s.mu.Lock()
	defer s.mu.Unlock()

	for _, ip := range addrs {
		key := "addr:" + ip.String()
		if _, exists := s.permanentFilterMap[key]; exists {
			continue
		}
		filterID, err := p.addWFPFirewallPermitFilter(s, ip)
		if err != nil {
			p.Warn().Err(err).Msgf("Firewall: failed to add permanent WFP permit for %s", ip)
			continue
		}
		s.permanentFilterMap[key] = filterID
		p.Debug().Msgf("Firewall: added permanent WFP permit for %s (ID: %d)", ip, filterID)
	}

	for _, prefix := range prefixes {
		key := "prefix:" + prefix.String()
		if _, exists := s.permanentFilterMap[key]; exists {
			continue
		}
		filterID, err := p.addWFPFirewallPermitPrefix(s, prefix)
		if err != nil {
			p.Warn().Err(err).Msgf("Firewall: failed to add permanent WFP permit for %s", prefix)
			continue
		}
		s.permanentFilterMap[key] = filterID
		p.Debug().Msgf("Firewall: added permanent WFP permit for %s (ID: %d)", prefix, filterID)
	}
}

// populateFilters installs permit filters for IPs already present in the allowlist
// before WFP callbacks were registered.
func (s *wfpFirewallState) populateFilters(p *prog) {
	if p.allowList == nil {
		return
	}
	ips := p.allowList.AllowedIPs()
	if len(ips) == 0 {
		return
	}

	s.mu.Lock()
	defer s.mu.Unlock()

	for _, ip := range ips {
		key := ip.String()
		if _, exists := s.filterMap[key]; exists {
			continue
		}

		filterID, err := p.addWFPFirewallPermitFilter(s, ip)
		if err != nil {
			p.Warn().Err(err).Msgf("Firewall: failed to add initial WFP permit filter for %s", key)
			continue
		}
		s.filterMap[key] = filterID
		p.Debug().Msgf("Firewall: added initial WFP permit filter for %s (ID: %d)", key, filterID)
	}
}

// addWFPFirewallBlockFilters installs the base block-all outbound filters.
// These block ALL non-loopback outbound TCP/UDP traffic. Per-IP permit filters
// (added dynamically from the allowlist) override these for resolved IPs.
func (p *prog) addWFPFirewallBlockFilters(fwState *wfpFirewallState) error {
	// Block all outbound IPv4 TCP/UDP.
	filterName, _ := windows.UTF16PtrFromString("ctrld Firewall Block All IPv4")
	filter := fwpmFilter0{
		subLayerKey: ctrldSubLayerGUID,
		weight: fwpValue0{
			valueType: fwpUint8, // FWP_UINT8
			value:     1,        // Must be lower than DNS permits (10). Firewall IP permits (5) override this.
		},
		action: fwpmAction0{
			actionType: fwpActionBlock, // FWP_ACTION_BLOCK
		},
		layerKey: fwpmLayerALEAuthConnectV4,
	}
	filter.displayData.name = filterName

	var filterID uint64
	r1, _, _ := procFwpmFilterAdd0.Call(
		fwState.engineHandle,
		uintptr(unsafe.Pointer(&filter)),
		0,
		uintptr(unsafe.Pointer(&filterID)),
	)
	if r1 != 0 {
		return fmt.Errorf("FwpmFilterAdd0 (block IPv4) failed: HRESULT 0x%x", r1)
	}
	fwState.blockFilterIDv4 = filterID

	// Block all outbound IPv6 TCP/UDP.
	filterNameV6, _ := windows.UTF16PtrFromString("ctrld Firewall Block All IPv6")
	filterV6 := fwpmFilter0{
		subLayerKey: ctrldSubLayerGUID,
		weight: fwpValue0{
			valueType: fwpUint8,
			value:     1,
		},
		action: fwpmAction0{
			actionType: fwpActionBlock,
		},
		layerKey: fwpmLayerALEAuthConnectV6,
	}
	filterV6.displayData.name = filterNameV6

	var filterIDv6 uint64
	r1, _, _ = procFwpmFilterAdd0.Call(
		fwState.engineHandle,
		uintptr(unsafe.Pointer(&filterV6)),
		0,
		uintptr(unsafe.Pointer(&filterIDv6)),
	)
	if r1 != 0 {
		if fwState.blockFilterIDv4 != 0 {
			deleteResult, _, _ := procFwpmFilterDeleteById0.Call(fwState.engineHandle, uintptr(fwState.blockFilterIDv4))
			if deleteResult != 0 {
				p.Debug().Msgf("Firewall: failed to roll back IPv4 block filter %d after IPv6 setup failure (HRESULT 0x%x)", fwState.blockFilterIDv4, deleteResult)
			}
			fwState.blockFilterIDv4 = 0
		}
		return fmt.Errorf("FwpmFilterAdd0 (block IPv6) failed: HRESULT 0x%x", r1)
	}
	fwState.blockFilterIDv6 = filterIDv6

	p.Info().Msgf("Firewall: WFP block-all filters installed (v4 ID: %d, v6 ID: %d)", filterID, filterIDv6)
	return nil
}

// addWFPFirewallPermitPrefix adds a WFP permit filter for a permanent CIDR prefix.
func (p *prog) addWFPFirewallPermitPrefix(fwState *wfpFirewallState, prefix netip.Prefix) (uint64, error) {
	prefix = prefix.Masked()
	if prefix.Addr().Is4() {
		return p.addWFPFirewallPermitIPv4Prefix(fwState, prefix)
	}
	return p.addWFPFirewallPermitIPv6Prefix(fwState, prefix)
}

func (p *prog) addWFPFirewallPermitIPv4Prefix(fwState *wfpFirewallState, prefix netip.Prefix) (uint64, error) {
	addr4 := prefix.Addr().As4()
	addr := uint32(addr4[0])<<24 | uint32(addr4[1])<<16 | uint32(addr4[2])<<8 | uint32(addr4[3])
	bits := prefix.Bits()
	var mask uint32
	if bits == 0 {
		mask = 0
	} else {
		mask = ^uint32(0) << uint(32-bits)
	}
	addrMask := fwpV4AddrAndMask{addr: addr & mask, mask: mask}

	filterName, _ := windows.UTF16PtrFromString("ctrld Firewall Permit " + prefix.String())
	condition := fwpmFilterCondition0{
		fieldKey:  fwpmConditionIPRemoteAddress,
		matchType: fwpMatchEqual,
	}
	condition.condValue.valueType = fwpV4AddrMask
	condition.condValue.value = uint64(uintptr(unsafe.Pointer(&addrMask)))

	filter := fwpmFilter0{
		subLayerKey:     ctrldSubLayerGUID,
		numFilterConds:  1,
		filterCondition: (*fwpmFilterCondition0)(unsafe.Pointer(&condition)),
		weight: fwpValue0{
			valueType: fwpUint8,
			value:     5,
		},
		action: fwpmAction0{
			actionType: fwpActionPermit,
		},
		layerKey: fwpmLayerALEAuthConnectV4,
	}
	filter.displayData.name = filterName

	var filterID uint64
	r1, _, _ := procFwpmFilterAdd0.Call(
		fwState.engineHandle,
		uintptr(unsafe.Pointer(&filter)),
		0,
		uintptr(unsafe.Pointer(&filterID)),
	)
	runtime.KeepAlive(&addrMask)
	if r1 != 0 {
		return 0, fmt.Errorf("FwpmFilterAdd0 (permit IPv4 prefix %s) failed: HRESULT 0x%x", prefix, r1)
	}
	return filterID, nil
}

func (p *prog) addWFPFirewallPermitIPv6Prefix(fwState *wfpFirewallState, prefix netip.Prefix) (uint64, error) {
	addrMask := fwpV6AddrAndMask{addr: prefix.Addr().As16(), prefixLength: uint8(prefix.Bits())}

	filterName, _ := windows.UTF16PtrFromString("ctrld Firewall Permit " + prefix.String())
	condition := fwpmFilterCondition0{
		fieldKey:  fwpmConditionIPRemoteAddress,
		matchType: fwpMatchEqual,
	}
	condition.condValue.valueType = fwpV6AddrMask
	condition.condValue.value = uint64(uintptr(unsafe.Pointer(&addrMask)))

	filter := fwpmFilter0{
		subLayerKey:     ctrldSubLayerGUID,
		numFilterConds:  1,
		filterCondition: (*fwpmFilterCondition0)(unsafe.Pointer(&condition)),
		weight: fwpValue0{
			valueType: fwpUint8,
			value:     5,
		},
		action: fwpmAction0{
			actionType: fwpActionPermit,
		},
		layerKey: fwpmLayerALEAuthConnectV6,
	}
	filter.displayData.name = filterName

	var filterID uint64
	r1, _, _ := procFwpmFilterAdd0.Call(
		fwState.engineHandle,
		uintptr(unsafe.Pointer(&filter)),
		0,
		uintptr(unsafe.Pointer(&filterID)),
	)
	runtime.KeepAlive(&addrMask)
	if r1 != 0 {
		return 0, fmt.Errorf("FwpmFilterAdd0 (permit IPv6 prefix %s) failed: HRESULT 0x%x", prefix, r1)
	}
	return filterID, nil
}

// addWFPFirewallPermitFilter adds a WFP permit filter for a single IP address.
// Returns the filter ID for later removal.
func (p *prog) addWFPFirewallPermitFilter(fwState *wfpFirewallState, ip netip.Addr) (uint64, error) {
	ip = ip.Unmap()

	if ip.Is4() {
		return p.addWFPFirewallPermitIPv4(fwState, ip)
	}
	return p.addWFPFirewallPermitIPv6(fwState, ip)
}

// addWFPFirewallPermitIPv4 adds a WFP permit filter for an IPv4 address.
func (p *prog) addWFPFirewallPermitIPv4(fwState *wfpFirewallState, ip netip.Addr) (uint64, error) {
	addr4 := ip.As4()
	ipUint32 := uint32(addr4[0])<<24 | uint32(addr4[1])<<16 | uint32(addr4[2])<<8 | uint32(addr4[3])

	filterName, _ := windows.UTF16PtrFromString("ctrld Firewall Permit " + ip.String())

	condition := fwpmFilterCondition0{
		fieldKey:  fwpmConditionIPRemoteAddress,
		matchType: fwpMatchEqual,
	}
	condition.condValue.valueType = fwpUint32
	condition.condValue.value = uint64(ipUint32)

	filter := fwpmFilter0{
		subLayerKey:     ctrldSubLayerGUID,
		numFilterConds:  1,
		filterCondition: (*fwpmFilterCondition0)(unsafe.Pointer(&condition)),
		weight: fwpValue0{
			valueType: fwpUint8,
			value:     5, // Higher than block-all (1), lower than DNS permits (10).
		},
		action: fwpmAction0{
			actionType: fwpActionPermit, // FWP_ACTION_PERMIT
		},
		layerKey: fwpmLayerALEAuthConnectV4,
	}
	filter.displayData.name = filterName

	var filterID uint64
	r1, _, _ := procFwpmFilterAdd0.Call(
		fwState.engineHandle,
		uintptr(unsafe.Pointer(&filter)),
		0,
		uintptr(unsafe.Pointer(&filterID)),
	)
	if r1 != 0 {
		return 0, fmt.Errorf("FwpmFilterAdd0 (permit IPv4 %s) failed: HRESULT 0x%x", ip, r1)
	}
	return filterID, nil
}

// addWFPFirewallPermitIPv6 adds a WFP permit filter for an IPv6 address.
func (p *prog) addWFPFirewallPermitIPv6(fwState *wfpFirewallState, ip netip.Addr) (uint64, error) {
	addr16 := ip.As16()

	filterName, _ := windows.UTF16PtrFromString("ctrld Firewall Permit " + ip.String())

	condition := fwpmFilterCondition0{
		fieldKey:  fwpmConditionIPRemoteAddress,
		matchType: fwpMatchEqual,
	}
	condition.condValue.valueType = fwpByteArray16Type
	condition.condValue.value = uint64(uintptr(unsafe.Pointer(&addr16)))

	filter := fwpmFilter0{
		subLayerKey:     ctrldSubLayerGUID,
		numFilterConds:  1,
		filterCondition: (*fwpmFilterCondition0)(unsafe.Pointer(&condition)),
		weight: fwpValue0{
			valueType: fwpUint8,
			value:     5, // Higher than block-all (1), lower than DNS permits (10).
		},
		action: fwpmAction0{
			actionType: fwpActionPermit,
		},
		layerKey: fwpmLayerALEAuthConnectV6,
	}
	filter.displayData.name = filterName

	var filterID uint64
	r1, _, _ := procFwpmFilterAdd0.Call(
		fwState.engineHandle,
		uintptr(unsafe.Pointer(&filter)),
		0,
		uintptr(unsafe.Pointer(&filterID)),
	)
	runtime.KeepAlive(addr16)
	if r1 != 0 {
		return 0, fmt.Errorf("FwpmFilterAdd0 (permit IPv6 %s) failed: HRESULT 0x%x", ip, r1)
	}
	return filterID, nil
}
