package failover

import (
	"context"
	"io"
	"net"

	"github.com/gofrs/uuid/v5"
	"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"
	E "github.com/sagernet/sing/common/exceptions"
	"github.com/sagernet/sing/common/logger"
	M "github.com/sagernet/sing/common/metadata"
	N "github.com/sagernet/sing/common/network"
	"github.com/sagernet/sing/common/uot"
	"github.com/sagernet/sing/service"
)

func RegisterOutbound(registry *outbound.Registry) {
	outbound.Register[option.FailoverOutboundOptions](registry, C.TypeFailover, NewFailover)
}

type Failover struct {
	outbound.Adapter
	ctx       context.Context
	outbound  adapter.OutboundManager
	logger    logger.ContextLogger
	dial      DialStrategy
	uotClient *uot.Client
}

func NewFailover(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.FailoverOutboundOptions) (adapter.Outbound, error) {
	if len(options.Outbounds) == 0 {
		return nil, E.New("missing outbounds")
	}
	outboundRegistry := service.FromContext[adapter.OutboundRegistry](ctx)
	outbounds := make([]adapter.Outbound, len(options.Outbounds))
	for i, outboundOptions := range options.Outbounds {
		outbound, err := outboundRegistry.UnsafeCreateOutbound(ctx, router, logger, outboundOptions.Tag, outboundOptions.Type, outboundOptions.Options)
		if err != nil {
			return nil, err
		}
		outbounds[i] = outbound
	}
	dial, err := CreateStrategy(options.Strategy, outbounds, logger, options.Delay.Build())
	if err != nil {
		return nil, err
	}
	outbound := &Failover{
		Adapter:  outbound.NewAdapter(C.TypeFailover, tag, []string{N.NetworkTCP, N.NetworkUDP}, []string{}),
		ctx:      ctx,
		outbound: service.FromContext[adapter.OutboundManager](ctx),
		logger:   logger,
		dial:     dial,
	}
	outbound.uotClient = &uot.Client{
		Dialer:  outbound,
		Version: uot.Version,
	}
	return outbound, nil
}

func (f *Failover) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
	if N.NetworkName(network) == N.NetworkUDP {
		return f.uotClient.DialContext(ctx, network, destination)
	}
	conn, err := f.dial(ctx, network, Destination)
	if err != nil {
		return nil, err
	}
	sessionUUID, err := uuid.NewV4()
	if err != nil {
		return nil, err
	}
	err = WriteRequest(conn, &Request{Command: CommandTCP, UUID: sessionUUID, Destination: destination})
	if err != nil {
		return nil, err
	}
	return NewFailoverConn(ctx, conn, func() (net.Conn, error) {
		ctx, cancel := context.WithTimeout(ctx, C.TCPConnectTimeout)
		defer cancel()
		conn, err := f.dial(ctx, network, Destination)
		if err != nil {
			return nil, err
		}
		err = WriteRequest(conn, &Request{Command: CommandReconnect, UUID: sessionUUID, Destination: destination})
		if err != nil {
			return nil, err
		}
		var data [1]byte
		_, err = io.ReadFull(conn, data[:])
		if err != nil {
			return nil, err
		}
		var status uint8 = data[0]
		if status == StatusSessionNotFound {
			conn.Close()
			return nil, SessionNotFound
		}
		return conn, nil
	}, nil), nil
}

func (f *Failover) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
	return f.uotClient.ListenPacket(ctx, destination)
}
