// Package jsonrpc2 provides a minimal JSON-RPC 2.0 HTTP client.
//
// The client is designed to work with the jsonrpc2 server implementation.
// It supports only single JSON-RPC 2.0 requests and notifications.
// Batch requests are explicitly unsupported.
//
// All request lifetimes are controlled by context.Context.
//
// Author: NiuJiuRu
// Email: niujiuru@qq.com
//
// See: https://www.jsonrpc.org/specification

package jsonrpc2

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"math"
	"net/http"
	"net/url"
	"sync/atomic"
)

// ClientOption configures an RPCClient.
type ClientOption func(*RPCClient)

// WithHTTPClient sets a custom http.Client.
func WithHTTPClient(hc *http.Client) ClientOption {
	return func(c *RPCClient) {
		if hc != nil {
			c.http = hc
		}
	}
}

// SetHeader sets a custom HTTP header for all requests.
func SetHeader(key, value string) ClientOption {
	return func(c *RPCClient) {
		if key == "" {
			return
		}
		if c.headers == nil {
			c.headers = make(http.Header)
		}
		c.headers.Set(key, value)
	}
}

// RPCClient is a minimal JSON-RPC 2.0 HTTP client.
type RPCClient struct {
	baseURL string
	http    *http.Client
	headers http.Header
	seq     atomic.Int64
}

// NewRPCClient creates a new JSON-RPC 2.0 client.
//
// The client is minimal, safe for concurrent use, and designed to work
// with standard JSON-RPC 2.0 HTTP servers.
//
// Example:
//
//	// Create a client with default settings.
//	client := jsonrpc2.NewRPCClient("http://localhost:8080/rpc")
//
//	// Create a client with a custom HTTP client and headers.
//	hc := &http.Client{
//		Timeout: 5 * time.Second,
//	}
//
//	client := jsonrpc2.NewRPCClient(
//		"http://localhost:8080/rpc",
//		jsonrpc2.WithHTTPClient(hc),
//		jsonrpc2.SetHeader("Authorization", "Bearer token"),
//		jsonrpc2.SetHeader("X-Request-ID", "req-001"),
//		jsonrpc2.SetHeader("User-Agent", "jsonrpc2-client/1.0"),
//	)
//
//	ctx := context.Background()
//	resp, err := client.Call(ctx, "add", []int{1, 2})
//
// Options are applied in order.
// If no http.Client is provided, http.DefaultClient is used.
func NewRPCClient(baseURL string, opts ...ClientOption) (*RPCClient, error) {
	if baseURL == "" {
		return nil, fmt.Errorf("baseURL cannot be empty")
	}

	_, err := url.Parse(baseURL)
	if err != nil {
		return nil, fmt.Errorf("invalid baseURL: %v", baseURL)
	}

	c := &RPCClient{
		baseURL: baseURL,
		http:    http.DefaultClient,
		headers: http.Header{},
	}

	for _, opt := range opts {
		opt(c)
	}
	return c, nil
}

// Call invokes a JSON-RPC method and returns the response.
func (c *RPCClient) Call(ctx context.Context, method string, params any) (*Response, error) {
	id := c.nextID()

	req, err := BuildRequest(method, params, id)
	if err != nil {
		return nil, fmt.Errorf("build request: %w", err)
	}

	return c.do(ctx, req)
}

// Notify sends a notification (no response expected).
func (c *RPCClient) Notify(ctx context.Context, method string, params any) error {
	req, err := BuildNotification(method, params)
	if err != nil {
		return fmt.Errorf("build notification: %w", err)
	}

	_, err = c.do(ctx, req)
	return err
}

func (c *RPCClient) nextID() int64 {
	for {
		id := c.seq.Add(1)
		if id > 0 && id <= math.MaxInt32 {
			return id
		}
		if c.seq.CompareAndSwap(id, 1) {
			return 1
		}
	}
}

func (c *RPCClient) do(ctx context.Context, req *Request) (*Response, error) {
	body, err := json.Marshal(req)
	if err != nil {
		return nil, fmt.Errorf("marshal request: %w", err)
	}

	httpReq, err := http.NewRequestWithContext(
		ctx,
		http.MethodPost,
		c.baseURL,
		bytes.NewReader(body),
	)
	if err != nil {
		return nil, fmt.Errorf("create request: %w", err)
	}

	httpReq.Header = c.headers.Clone()
	httpReq.Header.Set("Content-Type", "application/json")
	httpReq.Header.Set("Accept", "application/json")

	resp, err := c.http.Do(httpReq)
	if err != nil {
		return nil, fmt.Errorf("http request: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
	}

	if req.IsNotification() {
		_, _ = io.Copy(io.Discard, resp.Body)
		return nil, nil
	}

	data, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("read response: %w", err)
	}

	var rpcResp Response
	if err := json.Unmarshal(data, &rpcResp); err != nil {
		return nil, fmt.Errorf("unmarshal response: %w", err)
	}

	return &rpcResp, nil
}
