package healthcheck

import (
	"context"
	"crypto/tls"
	"errors"
	"net"
	"net/http"
	"net/url"
	"time"

	"github.com/valyala/fasthttp"
	"github.com/yusing/godoxy/internal/types"
	"github.com/yusing/goutils/version"
	"golang.org/x/net/http2"
)

var h2cClient = &http.Client{
	Transport: &http2.Transport{
		AllowHTTP: true,
		DialTLSContext: func(ctx context.Context, network, addr string, _ *tls.Config) (net.Conn, error) {
			var d net.Dialer
			return d.DialContext(ctx, network, addr)
		},
	},
}
var pinger = &fasthttp.Client{
	MaxConnDuration:               0,
	DisableHeaderNamesNormalizing: true,
	DisablePathNormalizing:        true,
	TLSConfig: &tls.Config{
		InsecureSkipVerify: true,
	},
	MaxConnsPerHost:          1,
	NoDefaultUserAgentHeader: true,
}

func HTTP(url *url.URL, method, path string, timeout time.Duration) (types.HealthCheckResult, error) {
	req := fasthttp.AcquireRequest()
	defer fasthttp.ReleaseRequest(req)

	resp := fasthttp.AcquireResponse()
	defer fasthttp.ReleaseResponse(resp)

	req.SetRequestURI(url.JoinPath(path).String())
	req.Header.SetMethod(method)
	setCommonHeaders(req.Header.Set)
	req.SetConnectionClose()

	start := time.Now()
	respErr := pinger.DoTimeout(req, resp, timeout)
	lat := time.Since(start)

	return processHealthResponse(lat, respErr, resp.StatusCode)
}

func H2C(ctx context.Context, url *url.URL, method, path string, timeout time.Duration) (types.HealthCheckResult, error) {
	u := url.JoinPath(path) // JoinPath returns a copy of the URL with the path joined
	u.Scheme = "http"

	ctx, cancel := context.WithTimeoutCause(ctx, timeout, errors.New("h2c health check timed out"))
	defer cancel()

	var req *http.Request
	var err error
	if method == fasthttp.MethodGet {
		req, err = http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
	} else {
		req, err = http.NewRequestWithContext(ctx, http.MethodHead, u.String(), nil)
	}
	if err != nil {
		return types.HealthCheckResult{
			Detail: err.Error(),
		}, nil
	}

	setCommonHeaders(req.Header.Set)

	client := *h2cClient
	client.Timeout = timeout

	start := time.Now()
	resp, err := client.Do(req)
	lat := time.Since(start)

	if resp != nil {
		defer resp.Body.Close()
	}

	return processHealthResponse(lat, err, func() int { return resp.StatusCode })
}

var userAgent = "GoDoxy/" + version.Get().String()

func setCommonHeaders(setHeader func(key, value string)) {
	setHeader("User-Agent", userAgent)
	setHeader("Accept", "text/plain,text/html,*/*;q=0.8")
	setHeader("Accept-Encoding", "identity")
	setHeader("Cache-Control", "no-cache")
	setHeader("Pragma", "no-cache")
}

func processHealthResponse(lat time.Duration, err error, getStatusCode func() int) (types.HealthCheckResult, error) {
	if err != nil {
		var tlsErr *tls.CertificateVerificationError
		if ok := errors.As(err, &tlsErr); !ok {
			return types.HealthCheckResult{
				Latency: lat,
				Detail:  err.Error(),
			}, nil
		}
		return types.HealthCheckResult{
			Latency: lat,
			Healthy: true,
			Detail:  tlsErr.Error(),
		}, nil
	}

	statusCode := getStatusCode()
	if statusCode >= 500 && statusCode < 600 {
		return types.HealthCheckResult{
			Latency: lat,
			Detail:  http.StatusText(statusCode),
		}, nil
	}

	return types.HealthCheckResult{
		Latency: lat,
		Healthy: true,
	}, nil
}
