package client

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"

	"github.com/deltaswapio/swaprouter/v3/log"
)

const (
	defaultSlowTimeout = 60 // seconds
	defaultTimeout     = 5  // seconds
	defaultRequestID   = 1
)

// Request json rpc request
type Request struct {
	Method  string
	Params  interface{}
	Timeout int
	ID      int
}

// GetDefaultTimeout get defaule timeout
func GetDefaultTimeout(slow bool) int {
	if slow {
		return defaultSlowTimeout
	}
	return defaultTimeout
}

// NewRequest new request
func NewRequest(method string, params ...interface{}) *Request {
	return NewRequestWithTimeoutAndID(defaultTimeout, defaultRequestID, method, params...)
}

// NewRequestWithTimeoutAndID new request with timeout and id
func NewRequestWithTimeoutAndID(timeout, id int, method string, params ...interface{}) *Request {
	req := &Request{
		Method:  method,
		Params:  params,
		Timeout: timeout,
		ID:      id,
	}
	if params == nil {
		// aurora chain can not accept null params,
		// replace with empty array here
		req.Params = []struct{}{}
	}
	return req
}

// RPCPost rpc post
func RPCPost(result interface{}, url, method string, params ...interface{}) error {
	req := NewRequest(method, params...)
	return RPCPostRequest(url, req, result)
}

// RPCPostWithTimeout rpc post with timeout
func RPCPostWithTimeout(timeout int, result interface{}, url, method string, params ...interface{}) error {
	req := NewRequestWithTimeoutAndID(timeout, defaultRequestID, method, params...)
	return RPCPostRequest(url, req, result)
}

// RPCPostWithContext rpc post with context
func RPCPostWithContext(ctx context.Context, result interface{}, url, method string, params ...interface{}) error {
	req := NewRequest(method, params...)
	return RPCPostRequestWithContext(ctx, url, req, result)
}

// RPCPostWithTimeoutAndID rpc post with timeout and id
func RPCPostWithTimeoutAndID(result interface{}, timeout, id int, url, method string, params ...interface{}) error {
	req := NewRequestWithTimeoutAndID(timeout, id, method, params...)
	return RPCPostRequest(url, req, result)
}

// RequestBody request body
type RequestBody struct {
	Version string      `json:"jsonrpc"`
	Method  string      `json:"method"`
	Params  interface{} `json:"params"`
	ID      int         `json:"id"`
}

type jsonError struct {
	Code    int         `json:"code"`
	Message string      `json:"message"`
	Data    interface{} `json:"data,omitempty"`
}

func (err *jsonError) Error() string {
	return fmt.Sprintf("json-rpc error %d, %s", err.Code, err.Message)
}

type jsonrpcResponse struct {
	Version string          `json:"jsonrpc,omitempty"`
	ID      json.RawMessage `json:"id,omitempty"`
	Error   *jsonError      `json:"error,omitempty"`
	Result  json.RawMessage `json:"result,omitempty"`
}

// RPCPostRequest rpc post request
func RPCPostRequest(url string, req *Request, result interface{}) error {
	return RPCPostRequestWithContext(httpCtx, url, req, result)
}

// RPCPostRequestWithContext rpc post request with context
func RPCPostRequestWithContext(ctx context.Context, url string, req *Request, result interface{}) error {
	reqBody := &RequestBody{
		Version: "2.0",
		Method:  req.Method,
		Params:  req.Params,
		ID:      req.ID,
	}
	resp, err := HTTPPostWithContext(ctx, url, reqBody, nil, nil, req.Timeout)
	if err != nil {
		log.Trace("post rpc error", "url", url, "request", req, "err", err)
		return err
	}
	err = getResultFromJSONResponse(result, resp)
	if err != nil {
		log.Trace("post rpc error", "url", url, "request", req, "err", err)
	}
	return err
}

func RPCPostBody(url string, params, headers map[string]string, body, result interface{}, timeout int, success_code map[int]bool) (errBody []byte, err error) {
	resp, err := HTTPPostWithContext(httpCtx, url, body, params, headers, timeout)
	if err != nil {
		log.Trace("post rpc error", "url", url, "request", body, "err", err)
		return errBody, err
	}
	defer func() {
		_ = resp.Body.Close()
	}()
	const maxReadContentLength int64 = 1024 * 1024 * 10 // 10M
	bodyBytes, err := ioutil.ReadAll(io.LimitReader(resp.Body, maxReadContentLength))
	if err != nil {
		return errBody, fmt.Errorf("read body error: %w", err)
	}
	if (success_code != nil && !success_code[resp.StatusCode]) ||
		(success_code == nil && resp.StatusCode != 200) {
		errBody = bodyBytes
		return errBody, fmt.Errorf("wrong response status %v", resp.StatusCode)
	}
	if len(bodyBytes) == 0 {
		return errBody, fmt.Errorf("empty response body")
	}
	err = json.Unmarshal(bodyBytes, &result)
	if err != nil {
		return errBody, fmt.Errorf("unmarshal body error, body is \"%v\" err=\"%w\"", string(bodyBytes), err)
	}
	return errBody, nil
}

func getResultFromJSONResponse(result interface{}, resp *http.Response) error {
	defer func() {
		_ = resp.Body.Close()
	}()
	const maxReadContentLength int64 = 1024 * 1024 * 10 // 10M
	body, err := ioutil.ReadAll(io.LimitReader(resp.Body, maxReadContentLength))
	if err != nil {
		return fmt.Errorf("read body error: %w", err)
	}
	if resp.StatusCode != 200 {
		return fmt.Errorf("wrong response status %v", resp.StatusCode)
	}
	if len(body) == 0 {
		return fmt.Errorf("empty response body")
	}

	var jsonResp jsonrpcResponse
	err = json.Unmarshal(body, &jsonResp)
	if err != nil {
		return fmt.Errorf("unmarshal body error, body is \"%v\" err=\"%w\"", string(body), err)
	}
	if jsonResp.Error != nil {
		return fmt.Errorf("return error: %w", jsonResp.Error)
	}
	err = json.Unmarshal(jsonResp.Result, &result)
	if err != nil {
		return fmt.Errorf("unmarshal result error: %w", err)
	}
	return nil
}

// RPCRawPost rpc raw post
func RPCRawPost(url, body string) (string, error) {
	return RPCRawPostWithTimeout(url, body, defaultSlowTimeout)
}

// RPCRawPostWithTimeout rpc raw post with timeout
func RPCRawPostWithTimeout(url, reqBody string, timeout int) (string, error) {
	resp, err := HTTPRawPost(url, reqBody, nil, nil, timeout)
	if err != nil {
		return "", err
	}
	defer func() {
		_ = resp.Body.Close()
	}()
	const maxReadContentLength int64 = 1024 * 1024 * 10 // 10M
	body, err := ioutil.ReadAll(io.LimitReader(resp.Body, maxReadContentLength))
	if err != nil {
		return "", fmt.Errorf("read body error: %w", err)
	}
	if resp.StatusCode != 200 {
		return "", fmt.Errorf("wrong response status %v", resp.StatusCode)
	}
	return string(body), nil
}

// RPCRawPostWithTimeout rpc raw post with timeout
func RPCJsonPostWithTimeout(url, reqBody string, timeout int) (string, error) {
	jsonStr := []byte(reqBody)
	if req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonStr)); err != nil {
		return "", err
	} else {
		req.Header.Set("Content-Type", "application/json")
		client := &http.Client{}
		if resp, err := client.Do(req); err != nil {
			return "", err
		} else {
			defer func() {
				_ = resp.Body.Close()
			}()
			const maxReadContentLength int64 = 1024 * 1024 * 10 // 10M
			if body, err := ioutil.ReadAll(io.LimitReader(resp.Body, maxReadContentLength)); err != nil {
				return "", fmt.Errorf("read body error: %w", err)
			} else {
				if resp.StatusCode != 200 {
					return "", fmt.Errorf("wrong response status %v", resp.StatusCode)
				}
				return string(body), nil
			}
		}
	}
}
