package adapter

import (
	"context"
	"net"
	"net/netip"
	"time"

	"github.com/sagernet/sing-tun"
	"github.com/sagernet/sing-tun/gtcpip/header"
	M "github.com/sagernet/sing/common/metadata"
	N "github.com/sagernet/sing/common/network"
	"github.com/sagernet/sing/common/x/list"

	"go4.org/netipx"
)

type Router interface {
	Lifecycle
	ConnectionRouter
	PreMatch(metadata InboundContext, firstPacket []byte) PreMatchResult
	HijackDNSPacket(ctx context.Context, payload []byte, writer N.PacketWriter, metadata InboundContext)
	ConnectionRouterEx
	RuleSet(tag string) (RuleSet, bool)
	Rules() []Rule
	NeedFindProcess() bool
	NeedFindNeighbor() bool
	NeighborResolver() NeighborResolver
	AppendTracker(tracker ConnectionTracker)
	ResetNetwork()
}

type PreMatchAction uint8

const (
	PreMatchContinue PreMatchAction = iota
	PreMatchFlow
	PreMatchReject
	PreMatchDrop
	PreMatchBypass
	PreMatchHijackDNS
)

type PreMatchResult struct {
	Action      PreMatchAction
	Outbound    Outbound
	Destination netip.AddrPort
	UDPTimeout  time.Duration
	NewTracker  func() tun.FlowTracker
}

func JudgeFlow(router Router, inbound string, inboundType string, network uint8, source netip.AddrPort, destination netip.AddrPort, firstPacket []byte) tun.FlowVerdict {
	var networkName string
	switch network {
	case uint8(header.TCPProtocolNumber):
		networkName = N.NetworkTCP
	case uint8(header.UDPProtocolNumber):
		networkName = N.NetworkUDP
	case uint8(header.ICMPv4ProtocolNumber), uint8(header.ICMPv6ProtocolNumber):
		networkName = N.NetworkICMP
	default:
		return tun.FlowVerdict{Action: tun.ActionAccept}
	}
	metadata := InboundContext{
		Inbound:     inbound,
		InboundType: inboundType,
		Network:     networkName,
		Source:      M.SocksaddrFromNetIP(source),
		Destination: M.SocksaddrFromNetIP(destination),
	}
	if networkName == N.NetworkICMP {
		metadata.Source.Port = 0
		metadata.Destination.Port = 0
	}
	result := router.PreMatch(metadata, firstPacket)
	switch result.Action {
	case PreMatchFlow:
		port, isPort := result.Outbound.(tun.Port)
		if !isPort {
			return tun.FlowVerdict{Action: tun.ActionAccept}
		}
		verdict := tun.FlowVerdict{Action: tun.ActionFlow, Port: port, UDPTimeout: result.UDPTimeout, NewTracker: result.NewTracker}
		if result.Destination.IsValid() {
			destinationPort := result.Destination.Port()
			if networkName == N.NetworkICMP {
				destinationPort = destination.Port()
			}
			verdict.Destination = netip.AddrPortFrom(result.Destination.Addr(), destinationPort)
		}
		return verdict
	case PreMatchReject:
		return tun.FlowVerdict{Action: tun.ActionReject}
	case PreMatchDrop:
		return tun.FlowVerdict{Action: tun.ActionDrop}
	case PreMatchBypass:
		return tun.FlowVerdict{Action: tun.ActionBypass}
	case PreMatchHijackDNS:
		return tun.FlowVerdict{Action: tun.ActionHijackDNS}
	default:
		return tun.FlowVerdict{Action: tun.ActionAccept}
	}
}

type ConnectionTracker interface {
	RoutedConnection(ctx context.Context, conn net.Conn, metadata InboundContext, matchedRule Rule, matchOutbound Outbound) net.Conn
	RoutedPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext, matchedRule Rule, matchOutbound Outbound) N.PacketConn
	RoutedFlow(ctx context.Context, metadata InboundContext, matchedRule Rule, matchOutbound Outbound) tun.FlowTracker
}

// Deprecated: Use ConnectionRouterEx instead.
type ConnectionRouter interface {
	RouteConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error
	RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
}

type ConnectionRouterEx interface {
	ConnectionRouter
	RouteConnectionEx(ctx context.Context, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc)
	RoutePacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata InboundContext, onClose N.CloseHandlerFunc)
}

type RuleSet interface {
	Name() string
	StartContext(ctx context.Context, startContext *HTTPStartContext) error
	Metadata() RuleSetMetadata
	ExtractIPSet() []*netipx.IPSet
	IncRef()
	DecRef()
	Cleanup()
	RegisterCallback(callback RuleSetUpdateCallback) *list.Element[RuleSetUpdateCallback]
	UnregisterCallback(element *list.Element[RuleSetUpdateCallback])
	Close() error
	HeadlessRule
}

type RuleSetUpdateCallback func(it RuleSet)

type DNSRuleSetUpdateValidator interface {
	ValidateRuleSetMetadataUpdate(tag string, metadata RuleSetMetadata) error
}

// ip_version is not a headless-rule item, so ContainsIPVersionRule is intentionally absent.
type RuleSetMetadata struct {
	ContainsProcessRule      bool
	ContainsWIFIRule         bool
	ContainsIPCIDRRule       bool
	ContainsDNSQueryTypeRule bool
	// ContainsNonIPCIDRRule signals that the rule-set carries at least one sub-rule
	// with a predicate other than destination ip_cidr / ip_set, so it can contribute
	// to DNS pre-response matching. A rule-set where this is false and
	// ContainsIPCIDRRule is true is "pure-IP" and matches nothing before a DNS
	// response is available.
	ContainsNonIPCIDRRule bool
}
