package health

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"
	"sync/atomic"
	"time"

	"github.com/prism/proxy/internal/cache"
	"github.com/prism/proxy/internal/store"
)

type ReloadFunc func(ctx context.Context) error

type Checker struct {
	store    *store.Store
	cache    *cache.Cache
	apiBase  string
	secret   string
	client   *http.Client
	testing  atomic.Bool
}

func NewChecker(s *store.Store, c *cache.Cache, apiBase, secret string) *Checker {
	return &Checker{
		store:   s,
		cache:   c,
		apiBase: stringsTrimRight(apiBase, "/"),
		secret:  secret,
		client: &http.Client{
			Timeout: 10 * time.Second,
		},
	}
}

func stringsTrimRight(s, cut string) string {
	for len(s) > 0 && stringsHasSuffix(s, cut) {
		s = s[:len(s)-len(cut)]
	}
	return s
}

func stringsHasSuffix(s, suffix string) bool {
	return len(s) >= len(suffix) && s[len(s)-len(suffix):] == suffix
}

func (h *Checker) TestNode(ctx context.Context, nodeID int64, reload ReloadFunc) (int, error) {
	node, err := h.store.GetProxyNode(ctx, nodeID)
	if err != nil {
		return -1, err
	}
	latency, err := h.testRuntimeName(node.RuntimeName)
	if isNotFound(err) && reload != nil {
		if reloadErr := reload(ctx); reloadErr == nil {
			latency, err = h.testRuntimeName(node.RuntimeName)
		}
	}
	if isNotFound(err) {
		err = fmt.Errorf("proxy %q not loaded in engine; reload engine in settings", node.RuntimeName)
	}
	alive := err == nil && latency >= 0
	lat := latency
	if !alive {
		lat = -1
	}
	_ = h.store.UpdateProxyNodeHealth(ctx, nodeID, lat, alive)
	h.cache.SetProxyLatency(node.RuntimeName, lat)
	return lat, err
}

func (h *Checker) TestAll(ctx context.Context, _ ReloadFunc) error {
	if !h.testing.CompareAndSwap(false, true) {
		return nil
	}
	go func() {
		defer h.testing.Store(false)
		nodes, err := h.store.ListProxyNodes(context.Background(), "", nil)
		if err != nil {
			return
		}
		for _, n := range nodes {
			if !n.Enabled {
				continue
			}
			_, _ = h.TestNode(context.Background(), n.ID, nil)
		}
	}()
	return nil
}

func (h *Checker) ProxyLoaded(name string) bool {
	data, err := h.getJSON("/proxies")
	if err != nil {
		return false
	}
	proxies, ok := data["proxies"].(map[string]any)
	if !ok {
		return false
	}
	_, ok = proxies[name]
	return ok
}

func (h *Checker) testRuntimeName(name string) (int, error) {
	testURL := fmt.Sprintf("%s/proxies/%s/delay?timeout=5000&url=%s", h.apiBase, url.PathEscape(name), url.QueryEscape("http://www.gstatic.com/generate_204"))
	req, err := http.NewRequest(http.MethodGet, testURL, nil)
	if err != nil {
		return -1, err
	}
	if h.secret != "" {
		req.Header.Set("Authorization", "Bearer "+h.secret)
	}
	resp, err := h.client.Do(req)
	if err != nil {
		return -1, err
	}
	defer resp.Body.Close()
	body, _ := io.ReadAll(resp.Body)
	if resp.StatusCode >= 400 {
		return -1, fmt.Errorf("delay test failed: %s", string(body))
	}
	var result struct {
		Delay int `json:"delay"`
	}
	if err := json.Unmarshal(body, &result); err != nil {
		return -1, err
	}
	return result.Delay, nil
}

func (h *Checker) getJSON(path string) (map[string]any, error) {
	req, err := http.NewRequest(http.MethodGet, h.apiBase+path, nil)
	if err != nil {
		return nil, err
	}
	if h.secret != "" {
		req.Header.Set("Authorization", "Bearer "+h.secret)
	}
	resp, err := h.client.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}
	if resp.StatusCode >= 400 {
		return nil, fmt.Errorf("api error %d: %s", resp.StatusCode, string(body))
	}
	var out map[string]any
	if err := json.Unmarshal(body, &out); err != nil {
		return nil, err
	}
	return out, nil
}

func isNotFound(err error) bool {
	return err != nil && strings.Contains(err.Error(), "Resource not found")
}

func (h *Checker) UpdateAPIBase(apiBase, secret string) {
	h.apiBase = stringsTrimRight(apiBase, "/")
	h.secret = secret
}
