// Copyright 2015 The go-ethereum Authors
// (original work)
// Copyright 2024 The Erigon Authors
// (modifications)
// This file is part of Erigon.
//
// Erigon is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Erigon is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with Erigon. If not, see <http://www.gnu.org/licenses/>.

package rpc

import (
	"bytes"
	"context"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"mime"
	"net/http"
	"net/url"
	"slices"
	"strings"
	"sync"
	"time"

	"github.com/golang-jwt/jwt/v4"

	"github.com/erigontech/erigon/common"
	"github.com/erigontech/erigon/common/dbg"
	"github.com/erigontech/erigon/common/log/v3"
	"github.com/erigontech/erigon/rpc/jsonstream"
)

const (
	maxRequestContentLength = 1024 * 1024 * 32 // 32MB
	contentType             = "application/json"
	jwtTokenExpiry          = 60 * time.Second
)

// https://www.jsonrpc.org/historical/json-rpc-over-http.html#id13
var acceptedContentTypes = []string{contentType, "application/json-rpc", "application/jsonrequest"}

type httpConn struct {
	client    *http.Client
	url       string
	closeOnce sync.Once
	closeCh   chan any
	mu        sync.Mutex // protects headers
	headers   http.Header
}

// httpConn implements ServerCodec, but it is treated specially by Client
// and some methods don't work. The panic() stubs here exist to ensure
// this special treatment is correct.

func (hc *httpConn) WriteJSON(context.Context, any) error {
	panic("writeJSON called on httpConn")
}

func (hc *httpConn) peerInfo() PeerInfo {
	panic("peerInfo called on httpConn")
}

func (hc *httpConn) remoteAddr() string {
	return hc.url
}

func (hc *httpConn) ReadBatch() ([]*jsonrpcMessage, bool, error) {
	<-hc.closeCh
	return nil, false, io.EOF
}

func (hc *httpConn) Close() {
	hc.closeOnce.Do(func() { close(hc.closeCh) })
}

func (hc *httpConn) closed() <-chan any {
	return hc.closeCh
}

// DialHTTPWithClient creates a new RPC client that connects to an RPC server over HTTP
// using the provided HTTP Client.
func DialHTTPWithClient(endpoint string, client *http.Client, logger log.Logger) (*Client, error) {
	// Sanity check URL so we don't end up with a client that will fail every request.
	_, err := url.Parse(endpoint)
	if err != nil {
		return nil, err
	}

	initctx := context.Background()
	headers := make(http.Header, 2)
	headers.Set("accept", contentType)
	headers.Set("content-type", contentType)
	return newClient(initctx, func(context.Context) (ServerCodec, error) {
		hc := &httpConn{
			client:  client,
			headers: headers,
			url:     endpoint,
			closeCh: make(chan any),
		}
		return hc, nil
	}, logger)
}

// DialHTTP creates a new RPC client that connects to an RPC server over HTTP.
func DialHTTP(endpoint string, logger log.Logger) (*Client, error) {
	client := &http.Client{Timeout: 30 * time.Second}
	return DialHTTPWithClient(endpoint, client, logger)
}

func (c *Client) sendHTTP(ctx context.Context, op *requestOp, msg any) error {
	hc := c.writeConn.(*httpConn)
	respBody, err := hc.doRequest(ctx, msg)
	if err != nil {
		return err
	}
	var respMsg jsonrpcMessage
	if err := json.Unmarshal(respBody, &respMsg); err != nil {
		return err
	}
	op.resp <- []*jsonrpcMessage{&respMsg}
	return nil
}

func (c *Client) sendBatchHTTP(ctx context.Context, op *requestOp, msgs []*jsonrpcMessage) error {
	hc := c.writeConn.(*httpConn)
	respBody, err := hc.doRequest(ctx, msgs)
	if err != nil {
		return err
	}
	var respMsgs []*jsonrpcMessage
	if err := json.Unmarshal(respBody, &respMsgs); err != nil {
		return err
	}
	op.resp <- respMsgs
	return nil
}

func (hc *httpConn) doRequest(ctx context.Context, msg any) ([]byte, error) {
	body, err := json.Marshal(msg)
	if err != nil {
		return nil, err
	}
	req, err := http.NewRequestWithContext(ctx, "POST", hc.url, io.NopCloser(bytes.NewReader(body)))
	if err != nil {
		return nil, err
	}
	req.ContentLength = int64(len(body))

	// set headers
	hc.mu.Lock()
	req.Header = hc.headers.Clone()
	hc.mu.Unlock()

	// do request
	resp, err := hc.client.Do(req)
	if err != nil {
		return nil, err
	}
	defer func() { _ = resp.Body.Close() }()

	// read the response body
	respBody, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}

	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return nil, fmt.Errorf("%s: %s", resp.Status, string(respBody))
	}

	if len(respBody) == 0 {
		return nil, errors.New("empty response from JSON-RPC server")
	}

	return respBody, nil
}

// httpServerConn turns a HTTP connection into a Conn.
type httpServerConn struct {
	io.Reader
	io.Writer
	r *http.Request
}

func newHTTPServerConn(r *http.Request, w http.ResponseWriter) ServerCodec {
	conn := &httpServerConn{Writer: w, r: r}
	// if the request is a GET request, and the body is empty, we turn the request into fake json rpc request, see below
	// https://www.jsonrpc.org/historical/json-rpc-over-http.html#encoded-parameters
	// we however allow for non base64 encoded parameters to be passed
	if r.Method == http.MethodGet && r.ContentLength == 0 {
		// default id 1
		id := `1`
		id_up := r.URL.Query().Get("id")
		if id_up != "" {
			id = id_up
		}
		method_up := r.URL.Query().Get("method")
		params, _ := url.QueryUnescape(r.URL.Query().Get("params"))
		param := []byte(params)
		if pb, err := base64.URLEncoding.DecodeString(params); err == nil {
			param = pb
		}
		buf := new(bytes.Buffer)
		json.NewEncoder(buf).Encode(jsonrpcMessage{
			ID:     json.RawMessage(id),
			Method: method_up,
			Params: param,
		})
		conn.Reader = buf
	} else {
		// it's a post request or whatever, so just process it like normal
		conn.Reader = io.LimitReader(r.Body, maxRequestContentLength)
	}
	return NewCodec(conn)
}

// Close does nothing and always returns nil.
func (t *httpServerConn) Close() error { return nil }

// RemoteAddr returns the peer address of the underlying connection.
func (t *httpServerConn) RemoteAddr() string {
	return t.r.RemoteAddr
}

// SetWriteDeadline does nothing and always returns nil.
func (t *httpServerConn) SetWriteDeadline(time.Time) error { return nil }

// httpOverloadedKey signals that the inner DB gate (kv.ErrReadTxLimitExceeded) rejected this request.
// ServeHTTP injects the *bool; runMethod sets it so the correct HTTP 503 status is written before flush.
type httpOverloadedKey struct{}

// httpFlusherContextKey carries a gzip-activation hook for the current request.
// It must only be set by the gzip middleware (not by a generic http.Flusher check),
// so that it is absent when gzip is disabled and cannot prematurely commit HTTP headers.
type httpFlusherContextKey struct{}

// WithGzipStreamingHook stores hook in ctx so that runMethod will call it before
// writing the first byte of a streamable response, switching the gzip middleware from
// one-shot buffering to incremental streaming. Must only be called by the gzip middleware.
func WithGzipStreamingHook(ctx context.Context, hook func()) context.Context {
	if hook == nil {
		panic("rpc: WithGzipStreamingHook called with a nil hook")
	}
	return context.WithValue(ctx, httpFlusherContextKey{}, hook)
}

// withoutGzipStreamingHook masks any gzip-streaming hook set on an ancestor context. Batch
// sub-calls write into a private per-item buffer rather than the HTTP response writer, so
// the hook must not fire for them; calling it concurrently from multiple batch goroutines is
// unsafe, since the underlying gzip.Writer it activates is not safe for concurrent use.
func withoutGzipStreamingHook(ctx context.Context) context.Context {
	return context.WithValue(ctx, httpFlusherContextKey{}, nil)
}

func withOverloadedFlag(ctx context.Context) (context.Context, *bool) {
	flag := new(bool)
	return context.WithValue(ctx, httpOverloadedKey{}, flag), flag
}

// SetOverloadedFlag marks the current HTTP request as DB-overloaded.
func SetOverloadedFlag(ctx context.Context) {
	if flag, _ := ctx.Value(httpOverloadedKey{}).(*bool); flag != nil {
		*flag = true
	}
}

// overloadedBody is precomputed; id is null because the request has not been parsed yet.
var overloadedBody = []byte(`{"jsonrpc":"2.0","id":null,"error":{"code":-32005,"message":"` + ErrMsgServerOverloaded + `"}}` + "\n")

// WriteOverloadedResponse writes HTTP 503 + JSON-RPC -32005 for the outer admission gate.
func WriteOverloadedResponse(w http.ResponseWriter) {
	w.Header().Set("Content-Type", contentType)
	w.Header().Set("Retry-After", "1")
	w.WriteHeader(http.StatusServiceUnavailable)
	_, _ = w.Write(overloadedBody)
}

// ServeHTTP serves JSON-RPC requests over HTTP.
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// Permit dumb empty requests for remote health-checks (AWS)
	if r.Method == http.MethodGet && r.ContentLength == 0 && r.URL.RawQuery == "" {
		w.WriteHeader(http.StatusOK)
		return
	}
	if code, err := validateRequest(r); err != nil {
		http.Error(w, err.Error(), code)
		return
	}

	// Don't serve if server is stopped.
	if !s.run.Load() {
		w.WriteHeader(http.StatusServiceUnavailable)
		return
	}

	// Create request-scoped context.
	connInfo := PeerInfo{Transport: "http", RemoteAddr: r.RemoteAddr}
	connInfo.HTTP.Version = r.Proto
	connInfo.HTTP.Host = r.Host
	connInfo.HTTP.Origin = r.Header.Get("Origin")
	connInfo.HTTP.UserAgent = r.Header.Get("User-Agent")
	ctx := r.Context()
	ctx = context.WithValue(ctx, peerInfoContextKey{}, connInfo)
	ctx, overloaded := withOverloadedFlag(ctx)
	// Note: the gzip-streaming hook (httpFlusherContextKey) is injected by the gzip
	// middleware via WithGzipStreamingHook, not here, to avoid prematurely committing
	// HTTP headers when gzip is disabled.

	// All checks passed, create a codec that reads directly from the request body
	// until EOF, writes the response to w, and orders the server to process a
	// single request.

	// The context might be cancelled if the client's connection was closed while waiting for ServeHTTP.
	if common.FastContextErr(ctx) != nil {
		// TODO: introduce an log message for all possible cases
		// s.logger.Warn("rpc.Server.ServeHTTP: client connection was lost. Check if the server is able to keep up with the request rate.", "url", r.URL.String())
		w.WriteHeader(http.StatusServiceUnavailable)
		return
	}

	if s.debugSingleRequest {
		if v := r.Header.Get(dbg.HTTPHeader); v == "true" {
			ctx = dbg.WithDebug(ctx, true)
		}
	}

	w.Header().Set("content-type", contentType)
	codec := newHTTPServerConn(r, w)
	defer codec.Close()
	var stream jsonstream.Stream
	if !s.disableStreaming {
		stream = jsonstream.New(w)
	}

	errorMsg := s.serveSingleRequest(ctx, codec, stream)
	if errorMsg != nil {
		w.WriteHeader(http.StatusBadRequest)
		codec.WriteJSON(ctx, errorMsg)
		return
	}

	if !s.disableStreaming {
		// If the inner DB gate rejected the request, the JSON-RPC error body is already
		// buffered in the stream. Set 503 before flushing so the status is correct.
		if *overloaded {
			w.Header().Set("Retry-After", "1")
			w.WriteHeader(http.StatusServiceUnavailable)
		}
		stream.Flush()
	}
}

// validateRequest returns a non-zero response code and error message if the
// request is invalid.
func validateRequest(r *http.Request) (int, error) {
	if r.Method == http.MethodPut || r.Method == http.MethodDelete {
		return http.StatusMethodNotAllowed, errors.New("method not allowed")
	}
	if r.ContentLength > maxRequestContentLength {
		err := fmt.Errorf("content length too large (%d>%d)", r.ContentLength, maxRequestContentLength)
		return http.StatusRequestEntityTooLarge, err
	}
	// Allow OPTIONS and GET (regardless of content-type)
	if r.Method == http.MethodOptions || r.Method == http.MethodGet {
		return 0, nil
	}
	// Check content-type
	if mt, _, err := mime.ParseMediaType(r.Header.Get("content-type")); err == nil {
		if slices.Contains(acceptedContentTypes, mt) {
			return 0, nil
		}
	}
	// Invalid content-type
	err := fmt.Errorf("invalid content type, only %s is supported", contentType)
	return http.StatusUnsupportedMediaType, err
}

func CheckJwtSecret(w http.ResponseWriter, r *http.Request, jwtSecret []byte) bool {
	var tokenStr string
	// Check if JWT signature is correct
	if auth := r.Header.Get("Authorization"); strings.HasPrefix(auth, "Bearer ") {
		tokenStr = strings.TrimPrefix(auth, "Bearer ")
	}

	if len(tokenStr) == 0 {
		http.Error(w, "missing token", http.StatusForbidden)
		return false
	}

	keyFunc := func(token *jwt.Token) (any, error) {
		return jwtSecret, nil
	}
	claims := jwt.RegisteredClaims{}
	// We explicitly set only HS256 allowed, and also disables the
	// claim-check: the RegisteredClaims internally requires 'iat' to
	// be no later than 'now', but we allow for a bit of drift.
	token, err := jwt.ParseWithClaims(tokenStr, &claims, keyFunc,
		jwt.WithValidMethods([]string{"HS256"}),
		jwt.WithoutClaimsValidation())

	switch {
	case err != nil:
		http.Error(w, err.Error(), http.StatusForbidden)
	case !token.Valid:
		http.Error(w, "invalid token", http.StatusForbidden)
	case !claims.VerifyExpiresAt(time.Now(), false): // optional
		http.Error(w, "token is expired", http.StatusForbidden)
	case claims.IssuedAt == nil:
		http.Error(w, "missing issued-at", http.StatusForbidden)
	case time.Since(claims.IssuedAt.Time) > jwtTokenExpiry:
		http.Error(w, "stale token", http.StatusForbidden)
	case time.Until(claims.IssuedAt.Time) > jwtTokenExpiry:
		http.Error(w, "future token", http.StatusForbidden)
	default:
		return true
	}

	return false
}
