package client

import (
	"bytes"
	"context"
	"encoding/json"
	"io"
	"net"
	"net/http"
	"net/url"
	"strconv"

	"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"
	CM "github.com/sagernet/sing-box/service/manager/constant"
	"github.com/sagernet/sing/common"
	E "github.com/sagernet/sing/common/exceptions"
	M "github.com/sagernet/sing/common/metadata"
)

type Client struct {
	boxService.Adapter

	ctx        context.Context
	logger     log.ContextLogger
	options    option.ManagerAPIClientOptions
	httpClient *http.Client
	baseURL    string
}

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
	}
	transport := &http.Transport{
		DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
			return outboundDialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
		},
	}
	scheme := "http"
	if options.TLS != nil {
		tlsConfig, err := tls.NewClient(ctx, logger, options.Server, common.PtrValueOrDefault(options.TLS))
		if err != nil {
			return nil, err
		}
		if !common.Contains(tlsConfig.NextProtos(), "http/1.1") {
			tlsConfig.SetNextProtos(append([]string{"http/1.1"}, tlsConfig.NextProtos()...))
		}
		transport.DialTLSContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
			rawConn, err := outboundDialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
			if err != nil {
				return nil, err
			}
			return tls.ClientHandshake(ctx, rawConn, tlsConfig)
		}
		scheme = "https"
	}
	return &Client{
		Adapter:    boxService.NewAdapter(C.TypeManagerAPI, tag),
		ctx:        ctx,
		logger:     logger,
		options:    options,
		httpClient: &http.Client{Transport: transport},
		baseURL:    scheme + "://" + options.ServerOptions.Build().String() + "/manager/v1",
	}, nil
}

func (s *Client) Start(stage adapter.StartStage) error { return nil }

func (s *Client) Close() error {
	s.httpClient.CloseIdleConnections()
	return nil
}

func (s *Client) doJSON(method, path string, query url.Values, body any, out any) error {
	var reqBody io.Reader
	if body != nil {
		b, err := json.Marshal(body)
		if err != nil {
			return err
		}
		reqBody = bytes.NewReader(b)
	}
	u := s.baseURL + path
	if len(query) > 0 {
		u += "?" + query.Encode()
	}
	req, err := http.NewRequestWithContext(s.ctx, method, u, reqBody)
	if err != nil {
		return err
	}
	if body != nil {
		req.Header.Set("Content-Type", "application/json")
	}
	if s.options.APIKey != "" {
		req.Header.Set("Authorization", "Bearer "+s.options.APIKey)
	}
	resp, err := s.httpClient.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode == http.StatusNotFound {
		return CM.ErrNotFound
	}
	if resp.StatusCode >= 400 {
		msg, _ := io.ReadAll(resp.Body)
		return E.New("manager-api http ", resp.StatusCode, ": ", string(msg))
	}
	if out != nil {
		return json.NewDecoder(resp.Body).Decode(out)
	}
	return nil
}

func filtersQuery(filters map[string][]string) url.Values {
	q := url.Values{}
	for k, vs := range filters {
		for _, v := range vs {
			q.Add(k, v)
		}
	}
	return q
}

func intPath(id int) string       { return "/" + strconv.Itoa(id) }
func stringPath(id string) string { return "/" + id }
