package client

import (
	"context"
	"net"
	"sync"

	"github.com/sagernet/sing-box/adapter"
	boxService "github.com/sagernet/sing-box/adapter/service"
	"github.com/sagernet/sing-box/common/dialer"
	"github.com/sagernet/sing-box/common/tls"
	C "github.com/sagernet/sing-box/constant"
	"github.com/sagernet/sing-box/log"
	"github.com/sagernet/sing-box/option"
	pb "github.com/sagernet/sing-box/service/manager_api/grpc/manager"
	"github.com/sagernet/sing/common"
	M "github.com/sagernet/sing/common/metadata"
	N "github.com/sagernet/sing/common/network"

	"google.golang.org/grpc"
	"google.golang.org/grpc/connectivity"
	"google.golang.org/grpc/credentials"
	"google.golang.org/grpc/credentials/insecure"
	"google.golang.org/grpc/metadata"
)

type Client struct {
	boxService.Adapter

	ctx     context.Context
	logger  log.ContextLogger
	dialer  N.Dialer
	creds   credentials.TransportCredentials
	options option.ManagerAPIClientOptions

	conn *grpc.ClientConn
	mtx  sync.Mutex
}

func NewClient(ctx context.Context, logger log.ContextLogger, tag string, options option.ManagerAPIClientOptions) (*Client, error) {
	outboundDialer, err := dialer.New(ctx, options.DialerOptions, options.ServerIsDomain())
	if err != nil {
		return nil, err
	}
	creds := insecure.NewCredentials()
	if options.TLS != nil {
		tlsConfig, err := tls.NewClient(ctx, logger, options.Server, common.PtrValueOrDefault(options.TLS))
		if err != nil {
			return nil, err
		}
		creds = &tlsCreds{config: tlsConfig}
	}
	return &Client{
		Adapter: boxService.NewAdapter(C.TypeManagerAPI, tag),
		ctx:     ctx,
		logger:  logger,
		dialer:  outboundDialer,
		creds:   creds,
		options: options,
	}, nil
}

func (s *Client) Start(stage adapter.StartStage) error { return nil }
func (s *Client) Close() error {
	s.mtx.Lock()
	defer s.mtx.Unlock()
	if s.conn != nil {
		return s.conn.Close()
	}
	return nil
}

func (s *Client) client() (pb.ManagerClient, error) {
	conn, err := s.getConn()
	if err != nil {
		return nil, err
	}
	return pb.NewManagerClient(conn), nil
}

func (s *Client) getConn() (*grpc.ClientConn, error) {
	s.mtx.Lock()
	defer s.mtx.Unlock()
	if s.conn != nil {
		state := s.conn.GetState()
		if state != connectivity.Shutdown && state != connectivity.TransientFailure {
			return s.conn, nil
		}
	}
	conn, err := grpc.NewClient(
		s.options.ServerOptions.Build().String(),
		grpc.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) {
			return s.dialer.DialContext(ctx, N.NetworkTCP, M.ParseSocksaddr(addr))
		}),
		grpc.WithTransportCredentials(s.creds),
	)
	if err != nil {
		return nil, err
	}
	s.conn = conn
	return conn, nil
}

func (s *Client) callContext() context.Context {
	if s.options.APIKey == "" {
		return s.ctx
	}
	return metadata.AppendToOutgoingContext(s.ctx, "authorization", s.options.APIKey)
}
