// Copyright (c) 2017-2026 Lethean (https://lt.hn)
//
// Licensed under the European Union Public Licence (EUPL) version 1.2.
// SPDX-License-Identifier: EUPL-1.2

// Package rpc provides a typed client for the Lethean daemon JSON-RPC API.
package rpc

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"time"

	coreerr "dappco.re/go/core/log"
)

// Client is a Lethean daemon RPC client.
type Client struct {
	url        string // Base URL with /json_rpc path for JSON-RPC calls.
	baseURL    string // Base URL without path for legacy calls.
	httpClient *http.Client
}

// NewClient creates a client for the daemon at the given URL.
// If the URL has no path, "/json_rpc" is appended automatically.
func NewClient(daemonURL string) *Client {
	return NewClientWithHTTP(daemonURL, &http.Client{Timeout: 30 * time.Second})
}

// NewClientWithHTTP creates a client with a custom http.Client.
func NewClientWithHTTP(daemonURL string, httpClient *http.Client) *Client {
	u, err := url.Parse(daemonURL)
	if err != nil {
		// Fall through with raw URL.
		return &Client{url: daemonURL + "/json_rpc", baseURL: daemonURL, httpClient: httpClient}
	}
	baseURL := fmt.Sprintf("%s://%s", u.Scheme, u.Host)
	if u.Path == "" || u.Path == "/" {
		u.Path = "/json_rpc"
	}
	return &Client{
		url:        u.String(),
		baseURL:    baseURL,
		httpClient: httpClient,
	}
}

// RPCError represents a JSON-RPC error returned by the daemon.
type RPCError struct {
	Code    int    `json:"code"`
	Message string `json:"message"`
}

func (e *RPCError) Error() string {
	return fmt.Sprintf("rpc error %d: %s", e.Code, e.Message)
}

// JSON-RPC 2.0 envelope types.
type jsonRPCRequest struct {
	JSONRPC string `json:"jsonrpc"`
	ID      string `json:"id"`
	Method  string `json:"method"`
	Params  any    `json:"params"`
}

type jsonRPCResponse struct {
	JSONRPC string          `json:"jsonrpc"`
	ID      json.RawMessage `json:"id"`
	Result  json.RawMessage `json:"result"`
	Error   *jsonRPCError   `json:"error,omitempty"`
}

type jsonRPCError struct {
	Code    int    `json:"code"`
	Message string `json:"message"`
}

// call makes a JSON-RPC 2.0 call to /json_rpc.
func (c *Client) call(method string, params any, result any) error {
	reqBody, err := json.Marshal(jsonRPCRequest{
		JSONRPC: "2.0",
		ID:      "0",
		Method:  method,
		Params:  params,
	})
	if err != nil {
		return coreerr.E("Client.call", "marshal request", err)
	}

	resp, err := c.httpClient.Post(c.url, "application/json", bytes.NewReader(reqBody))
	if err != nil {
		return coreerr.E("Client.call", fmt.Sprintf("post %s", method), err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return coreerr.E("Client.call", fmt.Sprintf("http %d from %s", resp.StatusCode, method), nil)
	}

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return coreerr.E("Client.call", "read response", err)
	}

	var rpcResp jsonRPCResponse
	if err := json.Unmarshal(body, &rpcResp); err != nil {
		return coreerr.E("Client.call", "unmarshal response", err)
	}

	if rpcResp.Error != nil {
		return &RPCError{Code: rpcResp.Error.Code, Message: rpcResp.Error.Message}
	}

	if result != nil && len(rpcResp.Result) > 0 {
		if err := json.Unmarshal(rpcResp.Result, result); err != nil {
			return coreerr.E("Client.call", "unmarshal result", err)
		}
	}
	return nil
}

// legacyCall makes a plain JSON POST to a legacy URI path (e.g. /getheight).
func (c *Client) legacyCall(path string, params any, result any) error {
	reqBody, err := json.Marshal(params)
	if err != nil {
		return coreerr.E("Client.legacyCall", "marshal request", err)
	}

	url := c.baseURL + path
	resp, err := c.httpClient.Post(url, "application/json", bytes.NewReader(reqBody))
	if err != nil {
		return coreerr.E("Client.legacyCall", fmt.Sprintf("post %s", path), err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return coreerr.E("Client.legacyCall", fmt.Sprintf("http %d from %s", resp.StatusCode, path), nil)
	}

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return coreerr.E("Client.legacyCall", "read response", err)
	}

	if result != nil {
		if err := json.Unmarshal(body, result); err != nil {
			return coreerr.E("Client.legacyCall", "unmarshal response", err)
		}
	}
	return nil
}
