package raft

import (
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"path"
	"strconv"
)

// HTTPTransport represents a transport for sending RPCs over the HTTP protocol.
type HTTPTransport struct{}

// Join requests membership into a node's cluster.
func (t *HTTPTransport) Join(uri url.URL, nodeURL url.URL) (uint64, uint64, *Config, error) {
	// Construct URL.
	u := uri
	u.Path = path.Join(u.Path, "raft/join")
	u.RawQuery = (&url.Values{"url": {nodeURL.String()}}).Encode()

	// Send HTTP request.
	resp, err := http.Get(u.String())
	if err != nil {
		return 0, 0, nil, err
	}
	defer func() { _ = resp.Body.Close() }()

	// Parse returned error.
	if s := resp.Header.Get("X-Raft-Error"); s != "" {
		return 0, 0, nil, errors.New(s)
	}

	// Parse returned id.
	id, err := strconv.ParseUint(resp.Header.Get("X-Raft-ID"), 10, 64)
	if err != nil {
		return 0, 0, nil, fmt.Errorf("invalid id: %q", resp.Header.Get("X-Raft-ID"))
	}

	// Parse returned id.
	leaderID, err := strconv.ParseUint(resp.Header.Get("X-Raft-Leader-ID"), 10, 64)
	if err != nil {
		return 0, 0, nil, fmt.Errorf("invalid leader id: %q", resp.Header.Get("X-Raft-Leader-ID"))
	}

	// Unmarshal config.
	var config *Config
	if err := json.NewDecoder(resp.Body).Decode(&config); err != nil {
		return 0, 0, nil, fmt.Errorf("config unmarshal: %s", err)
	}

	return id, leaderID, config, nil
}

// Leave removes a node from a cluster's membership.
func (t *HTTPTransport) Leave(uri url.URL, id uint64) error {
	// Construct URL.
	u := uri
	u.Path = path.Join(u.Path, "raft/leave")
	u.RawQuery = (&url.Values{"id": {strconv.FormatUint(id, 10)}}).Encode()

	// Send HTTP request.
	resp, err := http.Get(u.String())
	if err != nil {
		return err
	}
	defer func() { _ = resp.Body.Close() }()

	// Parse returned error.
	if s := resp.Header.Get("X-Raft-Error"); s != "" {
		return errors.New(s)
	}

	return nil
}

// Heartbeat checks the status of a follower.
func (t *HTTPTransport) Heartbeat(uri url.URL, term, commitIndex, leaderID uint64) (uint64, error) {
	// Construct URL.
	u := uri
	u.Path = path.Join(u.Path, "raft/heartbeat")

	// Set URL parameters.
	v := &url.Values{}
	v.Set("term", strconv.FormatUint(term, 10))
	v.Set("commitIndex", strconv.FormatUint(commitIndex, 10))
	v.Set("leaderID", strconv.FormatUint(leaderID, 10))
	u.RawQuery = v.Encode()

	// Send HTTP request.
	resp, err := http.Get(u.String())
	if err != nil {
		return 0, err
	}
	_ = resp.Body.Close()

	// Parse returned index.
	newIndexString := resp.Header.Get("X-Raft-Index")
	newIndex, err := strconv.ParseUint(newIndexString, 10, 64)
	if err != nil {
		return 0, fmt.Errorf("invalid index: %q", newIndexString)
	}

	// Parse returned error.
	if s := resp.Header.Get("X-Raft-Error"); s != "" {
		return newIndex, errors.New(s)
	}

	return newIndex, nil
}

// ReadFrom streams the log from a leader.
func (t *HTTPTransport) ReadFrom(uri url.URL, id, term, index uint64) (io.ReadCloser, error) {
	// Construct URL.
	u := uri
	u.Path = path.Join(u.Path, "raft/stream")

	// Set URL parameters.
	v := &url.Values{}
	v.Set("id", strconv.FormatUint(id, 10))
	v.Set("term", strconv.FormatUint(term, 10))
	v.Set("index", strconv.FormatUint(index, 10))
	u.RawQuery = v.Encode()

	// Send HTTP request.
	resp, err := http.Get(u.String())
	if err != nil {
		return nil, err
	}

	// Parse returned error.
	if s := resp.Header.Get("X-Raft-Error"); s != "" {
		_ = resp.Body.Close()
		return nil, errors.New(s)
	}

	return resp.Body, nil
}

// RequestVote requests a vote for a candidate in a given term.
func (t *HTTPTransport) RequestVote(uri url.URL, term, candidateID, lastLogIndex, lastLogTerm uint64) (uint64, error) {
	// Construct URL.
	u := uri
	u.Path = path.Join(u.Path, "raft/vote")

	// Set URL parameters.
	v := &url.Values{}
	v.Set("term", strconv.FormatUint(term, 10))
	v.Set("candidateID", strconv.FormatUint(candidateID, 10))
	v.Set("lastLogIndex", strconv.FormatUint(lastLogIndex, 10))
	v.Set("lastLogTerm", strconv.FormatUint(lastLogTerm, 10))
	u.RawQuery = v.Encode()

	// Send HTTP request.
	resp, err := http.Get(u.String())
	if err != nil {
		return 0, err
	}
	_ = resp.Body.Close()

	// Parse returned term.
	peerTerm, err := strconv.ParseUint(resp.Header.Get("X-Raft-Term"), 10, 64)
	if err != nil {
		return 0, fmt.Errorf("invalid returned term: %q", resp.Header.Get("X-Raft-Term"))
	}

	// Parse returned error.
	if s := resp.Header.Get("X-Raft-Error"); s != "" {
		return peerTerm, errors.New(s)
	}

	return peerTerm, nil
}
