package mempool

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strconv"
	"strings"
	"time"

	"github.com/gorilla/websocket"
)

var (
	ErrNotWebsocket = errors.New("provided addr was not websocket")
)

const (
	WSEndpoint = "api/v1/ws"
)

type mempool struct {
	baseUrl    string
	httpClient *http.Client
	ws         *websocket.Conn
}

func New(baseUrl string, httpClient *http.Client) *mempool {
	return &mempool{
		baseUrl: baseUrl,
		httpClient: &http.Client{
			Timeout: 5 * time.Second,
		},
	}
}

func (m *mempool) Init() error {
	u, err := url.Parse(fmt.Sprintf("wss://%s/%s", m.baseUrl, WSEndpoint))
	if err != nil {
		return err
	}

	if u.Scheme != "wss" && u.Scheme != "ws" {
		return fmt.Errorf("%w: got %s", ErrNotWebsocket, u.Scheme)
	}

	c, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
	if err != nil {
		return err
	}

	b, err := json.Marshal(struct {
		Action string   `json:"action"`
		Data   []string `json:"data"`
	}{
		Action: "want",
		Data:   []string{"blocks", "stats"},
	})
	if err != nil {
		return err
	}

	if err = c.WriteMessage(websocket.BinaryMessage, b); err != nil {
		return err
	}

	m.ws = c
	return nil
}

func (m *mempool) CurrentBlockHeight() (int, error) {
	req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("https://%s/api/blocks/tip/height", m.baseUrl), nil)
	if err != nil {
		return 0, err
	}

	resp, err := m.httpClient.Do(req)
	if err != nil {
		return 0, err
	}
	defer resp.Body.Close()

	if resp.StatusCode >= http.StatusInternalServerError {
		return 0, errors.New("server error")
	}

	if resp.StatusCode >= http.StatusBadRequest {
		return 0, errors.New("client error")
	}

	b, err := io.ReadAll(resp.Body)
	if err != nil {
		return 0, err
	}

	return strconv.Atoi(string(b))
}

func (m *mempool) CurrentFees() (int, int, int, error) {
	req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("https://%s/api/v1/fees/recommended", m.baseUrl), nil)
	if err != nil {
		return 0, 0, 0, err
	}

	resp, err := m.httpClient.Do(req)
	if err != nil {
		return 0, 0, 0, err
	}
	defer resp.Body.Close()

	if resp.StatusCode >= http.StatusInternalServerError {
		return 0, 0, 0, err
	}

	if resp.StatusCode >= http.StatusBadRequest {
		return 0, 0, 0, err
	}

	b, err := io.ReadAll(resp.Body)
	if err != nil {
		return 0, 0, 0, err
	}

	var data struct {
		Fastest  int `json:"fastestFee"`
		HalfHour int `json:"halfHourFee"`
		Hour     int `json:"hourFee"`
	}

	err = json.Unmarshal(b, &data)
	return data.Fastest, data.HalfHour, data.Hour, err
}

func (m *mempool) CurrentPrice() (int, error) {
	req, err := http.NewRequest(http.MethodGet, "https://api.coincap.io/v2/assets/bitcoin", nil)
	if err != nil {
		return 0, err
	}

	resp, err := m.httpClient.Do(req)
	if err != nil {
		return 0, err
	}
	defer resp.Body.Close()

	if resp.StatusCode >= http.StatusInternalServerError {
		return 0, err
	}

	if resp.StatusCode >= http.StatusBadRequest {
		return 0, err
	}

	b, err := io.ReadAll(resp.Body)
	if err != nil {
		return 0, err
	}

	var data struct {
		Data struct {
			PriceUSD string `json:"priceUsd"`
		} `json:"data"`
	}

	err = json.Unmarshal(b, &data)
	if err != nil {
		return 0, err
	}

	parts := strings.Split(data.Data.PriceUSD, ".")
	i, err := strconv.Atoi(parts[0])
	return i, err
}

func (m *mempool) Listen(ctx context.Context, messages chan Message, errs chan error, done chan struct{}) {
	for {
		select {
		case <-ctx.Done():
			close(messages)
			close(errs)
			close(done)
			return
		default:
			_, message, err := m.ws.ReadMessage()
			if err != nil {
				errs <- err
				continue
			}

			var m Message
			err = json.Unmarshal(message, &m)
			if err != nil {
				errs <- err
				continue
			}

			messages <- m
		}
	}
}

func (m *mempool) Shutdown(ctx context.Context) error {
	err := m.ws.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
	if err != nil {
		return err
	}
	m.ws.Close()
	return nil
}
