package bridge

import (
	"context"
	"net"
	"net/netip"
	"slices"

	"github.com/sagernet/sing-box/adapter"
	"github.com/sagernet/sing-box/adapter/outbound"
	C "github.com/sagernet/sing-box/constant"
	"github.com/sagernet/sing-box/log"
	"github.com/sagernet/sing-box/option"
	"github.com/sagernet/sing-tun"
	E "github.com/sagernet/sing/common/exceptions"
	M "github.com/sagernet/sing/common/metadata"
	N "github.com/sagernet/sing/common/network"
	"github.com/sagernet/sing/service"
)

func RegisterOutbound(registry *outbound.Registry) {
	outbound.Register[option.BridgeOutboundOptions](registry, C.TypeBridge, NewOutbound)
}

var (
	_ adapter.Outbound                    = (*Outbound)(nil)
	_ adapter.FlowOutbound                = (*Outbound)(nil)
	_ adapter.OutboundWithPreferredRoutes = (*Outbound)(nil)
	_ adapter.Lifecycle                   = (*Outbound)(nil)
)

type Backend interface {
	adapter.Lifecycle
	tun.Port
}

type Outbound struct {
	outbound.Adapter
	logger            log.ContextLogger
	networkManager    adapter.NetworkManager
	platformInterface adapter.PlatformInterface
	backend           Backend
}

func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.BridgeOutboundOptions) (adapter.Outbound, error) {
	networkManager := service.FromContext[adapter.NetworkManager](ctx)
	outboundBackend, err := newBackend(ctx, logger, networkManager, tag, options)
	if err != nil {
		return nil, err
	}
	return &Outbound{
		Adapter:           outbound.NewAdapter(C.TypeBridge, tag, []string{N.NetworkTCP, N.NetworkUDP, N.NetworkICMP}, nil),
		logger:            logger,
		networkManager:    networkManager,
		platformInterface: service.FromContext[adapter.PlatformInterface](ctx),
		backend:           outboundBackend,
	}, nil
}

func (o *Outbound) Start(stage adapter.StartStage) error {
	return o.backend.Start(stage)
}

func (o *Outbound) Close() error {
	return o.backend.Close()
}

func (o *Outbound) PreferredDomain(metadata *adapter.InboundContext, domain string) bool {
	return false
}

func (o *Outbound) PreferredAddress(metadata *adapter.InboundContext, address netip.Addr) bool {
	return metadata.PreMatch && !o.isLocalDestination(address)
}

func (o *Outbound) PreMatchFlow(network string, destination netip.Addr) adapter.PreMatchAction {
	if o.isLocalDestination(destination) {
		o.logger.Warn("rejected connection to local destination ", destination, ": traffic to local addresses is not supported by bridge, exclude them in route rules")
		return adapter.PreMatchReject
	}
	return adapter.PreMatchFlow
}

func (o *Outbound) isLocalDestination(destination netip.Addr) bool {
	if !destination.IsValid() {
		return false
	}
	destination = destination.Unmap()
	if destination.IsLoopback() || destination.IsUnspecified() {
		return true
	}
	if o.platformInterface != nil && slices.Contains(o.platformInterface.MyInterfaceAddress(), destination) {
		return true
	}
	for _, netInterface := range o.networkManager.InterfaceFinder().Interfaces() {
		for _, prefix := range netInterface.Addresses {
			if prefix.Addr() == destination {
				return true
			}
		}
	}
	return false
}

func (o *Outbound) PortAddresses() (netip.Addr, netip.Addr) {
	return o.backend.PortAddresses()
}

func (o *Outbound) PortMTU() uint32 {
	return o.backend.PortMTU()
}

func (o *Outbound) PortSelectorRange() (uint16, uint16) {
	if rangedBackend, isRanged := o.backend.(tun.PortWithSelectorRange); isRanged {
		return rangedBackend.PortSelectorRange()
	}
	return 0, 0
}

func (o *Outbound) AttachReturn(returnPath tun.Return) error {
	return o.backend.AttachReturn(returnPath)
}

func (o *Outbound) DetachReturn(returnPath tun.Return) error {
	return o.backend.DetachReturn(returnPath)
}

func (o *Outbound) WritePackets(packets [][]byte) error {
	return o.backend.WritePackets(packets)
}

func (o *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
	return nil, E.New("only L3 traffic is supported by bridge")
}

func (o *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
	return nil, E.New("only L3 traffic is supported by bridge")
}
