package internal

import (
	"bytes"
	"context"
	"crypto/tls"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"math/rand"
	"net/http"
	"net/http/httputil"
	"net/url"
	"reflect"
	"strconv"
	"strings"
	"time"

	"github.com/google/uuid"
)

type SvixHttpClient struct {
	DefaultHeaders map[string]string
	HTTPClient     *http.Client
	RetrySchedule  []time.Duration
	BaseURL        string
	Debug          bool
}

func DefaultSvixHttpClient(defaultBaseUrl string) SvixHttpClient {
	// Disable HTTP/2.0
	tr := http.DefaultTransport.(*http.Transport).Clone()
	tr.ForceAttemptHTTP2 = false
	tr.TLSClientConfig = new(tls.Config)
	tr.TLSNextProto = make(map[string]func(authority string, c *tls.Conn) http.RoundTripper)

	return SvixHttpClient{
		DefaultHeaders: map[string]string{},
		HTTPClient: &http.Client{
			Timeout:   60 * time.Second,
			Transport: tr,
		},
		RetrySchedule: []time.Duration{50 * time.Microsecond, 100 * time.Microsecond, 200 * time.Microsecond},
		BaseURL:       defaultBaseUrl,
		Debug:         false,
	}
}

// requestURL joins the client's base URL with a request path.
//
// Trailing slashes are stripped from the base URL, so a server URL of
// `https://api.svix.com/` behaves the same as `https://api.svix.com`. The
// request path always starts with a slash and is left alone.
func requestURL(client *SvixHttpClient, path string, pathParams map[string]string) string {
	return strings.TrimRight(client.BaseURL, "/") + replacePathKeys(path, pathParams)
}

func ExecuteRequest[ReqBody any, ResBody any](
	ctx context.Context,
	client *SvixHttpClient,
	method string,
	path string,
	pathParams map[string]string,
	queryParams map[string]string,
	headerParams map[string]string,
	jsonBody *ReqBody,

) (*ResBody, error) {

	urlWithPath := requestURL(client, path, pathParams)
	urlStr, err := addQueryParams(urlWithPath, queryParams)
	if err != nil {
		return nil, err
	}
	var req *http.Request
	if jsonBody != nil {
		encodedBody, err := json.Marshal(jsonBody)
		if err != nil {
			return nil, err
		}
		req, err = http.NewRequestWithContext(ctx, method, urlStr, bytes.NewBuffer(encodedBody))
		if err != nil {
			return nil, err
		}
		req.Header.Set("content-type", "application/json")
	} else {
		req, err = http.NewRequestWithContext(ctx, method, urlStr, &bytes.Buffer{})
		if err != nil {
			return nil, err
		}

	}

	key, ok := headerParams["idempotency-key"]
	if (!ok || key == "") && strings.ToUpper(method) == "POST" {
		req.Header.Set("idempotency-key", "auto_"+uuid.New().String())
	}

	req.Header.Set("svix-req-id", strconv.FormatUint(rand.Uint64(), 10))
	for hKey, hVal := range client.DefaultHeaders {
		req.Header.Add(hKey, hVal)
	}
	for hKey, hVal := range headerParams {
		req.Header.Add(hKey, hVal)
	}

	res, err := executeRequestWithRetries(client, req)

	if err != nil {
		return nil, err
	}
	if res.StatusCode == 204 {
		return nil, nil
	}
	defer res.Body.Close()

	body, err := io.ReadAll(res.Body)
	if err != nil {
		return nil, err
	}
	if res.StatusCode >= 200 && res.StatusCode <= 299 {
		var ret ResBody
		err = json.Unmarshal(body, &ret)
		if err != nil {
			return nil, err
		}

		return &ret, nil
	}
	return nil, &Error{
		status: res.StatusCode,
		body:   body,
		error:  fmt.Sprintf("status code %s", res.Status),
	}

}

func executeRequestWithRetries(client *SvixHttpClient, request *http.Request) (*http.Response, error) {
	var bodyBytes []byte
	if request.Body != nil {
		var err error
		bodyBytes, err = io.ReadAll(request.Body)
		if err != nil {
			return nil, err
		}
		err = request.Body.Close()
		if err != nil {
			return nil, err
		}
	}

	if bodyBytes != nil {
		request.Body = io.NopCloser(bytes.NewReader(bodyBytes))
	}

	if client.Debug {
		log.Printf("URL: %s", request.URL)
		dump, err := httputil.DumpRequestOut(request, true)
		if err != nil {
			return nil, err
		}
		log.Printf("\n%s\n", string(dump))
	}

	resp, err := client.HTTPClient.Do(request)
	for try := 0; try < len(client.RetrySchedule); try++ {
		if err == nil && resp.StatusCode < 500 {
			break
		}
		if bodyBytes != nil {
			request.Body = io.NopCloser(bytes.NewReader(bodyBytes))
		}
		request.Header.Set("svix-retry-count", strconv.Itoa(try+1))
		sleepTime := client.RetrySchedule[try]
		time.Sleep(sleepTime)
		resp, err = client.HTTPClient.Do(request)
	}

	if client.Debug {
		if resp != nil {
			dump, err := httputil.DumpResponse(resp, true)
			if err != nil {
				return resp, err
			}
			log.Printf("\n%s\n", string(dump))
		}
	}
	return resp, err
}

func replacePathKeys(path string, pathParams map[string]string) string {
	for key, value := range pathParams {
		placeholder := "{" + key + "}"
		path = strings.ReplaceAll(path, placeholder, value)
	}
	return path
}

func addQueryParams(baseURL string, params map[string]string) (string, error) {
	parsedURL, err := url.Parse(baseURL)
	if err != nil {
		return "", err
	}

	query := parsedURL.Query()
	for key, value := range params {
		query.Add(key, value)
	}

	parsedURL.RawQuery = query.Encode()
	return parsedURL.String(), nil
}

func SerializeParamToMap(key string, val interface{}, d map[string]string, err *error) {
	// I pass the error in here so I don't have to "if err != nil" for every query param
	if *err != nil {
		return
	}
	// If val is null don't add it to the query params map
	if val == nil || (reflect.ValueOf(val).Kind() == reflect.Ptr && reflect.ValueOf(val).IsNil()) {
		switch key {
		case "with_content":
			// default with_content to false, it only defaults to true server-side
			// because of backwards-compatibility
			d[key] = "false"
		case "expanded_statuses":
			// default expanded_statuses to true, it only defaults to false
			// server-side because of backwards-compatibility
			d[key] = "true"
		}

		return
	}

	v, localErr := serializeQueryOrHeaderParam(val, key)
	if localErr != nil {
		*err = localErr
	} else {
		d[key] = v
	}
}

func serializeQueryOrHeaderParam(val interface{}, key string) (string, error) {
	v := reflect.ValueOf(val)
	var value string
	if val == nil || (reflect.ValueOf(val).Kind() == reflect.Ptr && reflect.ValueOf(val).IsNil()) {
		return "", fmt.Errorf("can't serialize nil as a query param, key: %s", key)
	}

	switch v.Kind() {
	case reflect.Pointer:
		innerVal, err := serializeQueryOrHeaderParam(v.Elem().Interface(), key)
		if err != nil {
			return "", err
		}
		value = innerVal
	case reflect.String:
		value = v.String()
	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
		value = strconv.FormatInt(v.Int(), 10)
	case reflect.Uint, reflect.Uint8, reflect.Uint16,
		reflect.Uint32, reflect.Uint64:
		value = strconv.FormatUint(v.Uint(), 10)
	case reflect.Float32, reflect.Float64:
		value = strconv.FormatFloat(v.Float(), 'g', -1, 64)
	case reflect.Bool:
		if v.Bool() {
			value = "true"
		} else {
			value = "false"
		}
	case reflect.Slice:
		// we are assuming that the inner type is a simple type (no nested lists)
		serializedValues := make([]string, v.Len())
		for i := 0; i < v.Len(); i++ {
			serializedVal, err := serializeQueryOrHeaderParam(v.Index(i).Interface(), key)
			if err != nil {
				return "", err
			}
			serializedValues[i] = serializedVal
		}
		value = strings.Join(serializedValues, ",")
	case reflect.Struct:
		// if it's a time.time
		if t, ok := v.Interface().(time.Time); ok {
			return t.Format(time.RFC3339), nil
		}

		// else fallthrough
		fallthrough
	default:
		return "", fmt.Errorf("can't serialize %s as a query param, key: %s", v.Kind().String(), key)
	}

	return value, nil
}
