package cmd

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net"
	"net/http"
	"os"
	"time"

	"github.com/ditatompel/xmr-nodes/internal/config"
	"github.com/ditatompel/xmr-nodes/internal/repo"

	"github.com/spf13/cobra"
	"golang.org/x/net/proxy"
)

const RPCUserAgent = "ditatombot/0.0.1 (Monero RPC Monitoring; Contact: ditatombot@ditatompel.com)"

type proberClient struct {
	config  *config.App
	message string
}

func newProber(cfg *config.App) *proberClient {
	return &proberClient{config: cfg}
}

var probeCmd = &cobra.Command{
	Use:   "probe",
	Short: "Run Monero node prober",
	Run: func(_ *cobra.Command, _ []string) {
		runProbe()
	},
}

func init() {
	rootCmd.AddCommand(probeCmd)
}

func runProbe() {
	cfg := config.AppCfg()
	if cfg.ServerEndpoint == "" {
		fmt.Println("Please set SERVER_ENDPOINT in .env")
		os.Exit(1)
	}
	fmt.Printf("Accept Tor: %t\n", cfg.AcceptTor)

	if cfg.AcceptTor && cfg.TorSocks == "" {
		fmt.Println("Please set TOR_SOCKS in .env")
		os.Exit(1)
	}

	probe := newProber(cfg)

	node, err := probe.getJob()
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}

	fetchNode, err := probe.fetchNode(node)
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
	fmt.Println(prettyPrint(fetchNode))
}

func (p *proberClient) getJob() (repo.MoneroNode, error) {
	queryParams := ""
	if p.config.ApiKey != "" {
		queryParams = "?api_key=" + p.config.ApiKey
	}

	node := repo.MoneroNode{}

	endpoint := fmt.Sprintf("%s/api/v1/job%s", p.config.ServerEndpoint, queryParams)

	req, err := http.NewRequest(http.MethodGet, endpoint, nil)
	if err != nil {
		return node, err
	}
	req.Header.Add("X-Prober-Api-Key", p.config.ApiKey)
	req.Header.Set("User-Agent", RPCUserAgent)

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return node, err
	}
	defer resp.Body.Close()

	if resp.StatusCode != 200 {
		return node, fmt.Errorf("status code: %d", resp.StatusCode)
	}

	response := struct {
		Data repo.MoneroNode `json:"data"`
	}{}

	err = json.NewDecoder(resp.Body).Decode(&response)
	if err != nil {
		return node, err
	}

	node = response.Data

	return node, nil
}

func (p *proberClient) fetchNode(node repo.MoneroNode) (repo.MoneroNode, error) {
	startTime := time.Now()
	endpoint := fmt.Sprintf("%s://%s:%d/json_rpc", node.Protocol, node.Hostname, node.Port)
	rpcParam := []byte(`{"jsonrpc": "2.0","id": "0","method": "get_info"}`)

	req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewBuffer(rpcParam))
	if err != nil {
		return node, err
	}
	req.Header.Set("Content-Type", "application/json; charset=UTF-8")
	req.Header.Set("User-Agent", RPCUserAgent)
	req.Header.Set("Origin", "https://xmr.ditatompel.com")

	var client http.Client
	if p.config.AcceptTor && node.IsTor {
		dialer, err := proxy.SOCKS5("tcp", p.config.TorSocks, nil, proxy.Direct)
		if err != nil {
			return node, err
		}
		dialContext := func(ctx context.Context, network, addr string) (net.Conn, error) {
			return dialer.Dial(network, addr)
		}
		transport := &http.Transport{
			DialContext:       dialContext,
			DisableKeepAlives: true,
		}
		client.Transport = transport
		client.Timeout = 60 * time.Second
	}

	// reset the default node struct
	node.IsAvailable = false

	resp, err := client.Do(req)
	if err != nil {
		p.message = err.Error()
		p.reportResult(node, time.Since(startTime).Seconds())
		return node, err
	}
	defer resp.Body.Close()

	if resp.StatusCode != 200 {
		p.message = fmt.Sprintf("status code: %d", resp.StatusCode)
		p.reportResult(node, time.Since(startTime).Seconds())
		return node, errors.New(p.message)
	}

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		p.message = err.Error()
		p.reportResult(node, time.Since(startTime).Seconds())
		return node, err
	}

	reportNode := struct {
		repo.MoneroNode `json:"result"`
	}{}

	if err := json.Unmarshal(body, &reportNode); err != nil {
		p.message = err.Error()
		p.reportResult(node, time.Since(startTime).Seconds())
		return node, err
	}
	if reportNode.Status == "OK" {
		node.IsAvailable = true
	}
	node.NetType = reportNode.NetType
	node.AdjustedTime = reportNode.AdjustedTime
	node.DatabaseSize = reportNode.DatabaseSize
	node.Difficulty = reportNode.Difficulty
	node.Height = reportNode.Height
	node.Version = reportNode.Version

	if resp.Header.Get("Access-Control-Allow-Origin") == "*" || resp.Header.Get("Access-Control-Allow-Origin") == "https://xmr.ditatompel.com" {
		node.CorsCapable = true
	}

	if !node.IsTor {
		hostIp, err := net.LookupIP(node.Hostname)
		if err != nil {
			fmt.Println("Warning: Could not resolve hostname: " + node.Hostname)
		} else {
			node.Ip = hostIp[0].String()
		}
	}

	// Sleeping 1 second to avoid too many request on host behind CloudFlare
	// time.Sleep(1 * time.Second)

	// check fee
	rpcCheckFeeParam := []byte(`{"jsonrpc": "2.0","id": "0","method": "get_fee_estimate"}`)
	reqCheckFee, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewBuffer(rpcCheckFeeParam))
	if err != nil {
		return node, err
	}
	reqCheckFee.Header.Set("Content-Type", "application/json; charset=UTF-8")
	reqCheckFee.Header.Set("User-Agent", RPCUserAgent)

	checkFee, err := client.Do(reqCheckFee)
	if err != nil {
		return node, err
	}
	defer checkFee.Body.Close()

	if checkFee.StatusCode != 200 {
		return node, fmt.Errorf("status code: %d", checkFee.StatusCode)
	}

	bodyCheckFee, err := io.ReadAll(checkFee.Body)
	if err != nil {
		return node, err
	}

	feeEstimate := struct {
		Result struct {
			Fee uint `json:"fee"`
		} `json:"result"`
	}{}

	if err := json.Unmarshal(bodyCheckFee, &feeEstimate); err != nil {
		return node, err
	}

	tookTime := time.Since(startTime).Seconds()
	node.EstimateFee = feeEstimate.Result.Fee

	fmt.Printf("Took %f seconds\n", tookTime)

	if err := p.reportResult(node, tookTime); err != nil {
		return node, err
	}
	return node, nil
}

func (p *proberClient) reportResult(node repo.MoneroNode, tookTime float64) error {
	jsonData, err := json.Marshal(repo.ProbeReport{
		TookTime: tookTime,
		Message:  p.message,
		NodeInfo: node,
	})
	if err != nil {
		return err
	}

	endpoint := fmt.Sprintf("%s/api/v1/job", p.config.ServerEndpoint)
	req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewBuffer(jsonData))
	if err != nil {
		return err
	}
	req.Header.Add("X-Prober-Api-Key", p.config.ApiKey)
	req.Header.Set("Content-Type", "application/json; charset=UTF-8")
	req.Header.Set("User-Agent", RPCUserAgent)

	client := &http.Client{Timeout: 60 * time.Second}
	resp, err := client.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode != 200 {
		return fmt.Errorf("status code: %d", resp.StatusCode)
	}
	return nil
}

// for debug purposes
func prettyPrint(i interface{}) string {
	s, _ := json.MarshalIndent(i, "", "\t")
	return string(s)
}