package common

import (
	"bytes"
	"compress/gzip"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"log"
	"net/http"
	"net/url"
	"strings"

	lm "github.com/hrfee/jfa-go/logmessages"
)

// TimeoutHandler recovers from an http timeout or panic.
type TimeoutHandler func()

// NewTimeoutHandler returns a new Timeout handler.
func NewTimeoutHandler(name, addr string, noFail bool) TimeoutHandler {
	return func() {
		if r := recover(); r != nil {
			out := fmt.Sprintf(lm.FailedAuth, name, addr, 0, lm.TimedOut)
			if noFail {
				log.Print(out)
			} else {
				log.Fatalf(out)
			}
		}
	}
}

// most 404 errors are from UserNotFound, so this generic error doesn't really need any detail.
type ErrNotFound error

type ErrUnauthorized struct{}

func (err ErrUnauthorized) Error() string {
	return lm.Unauthorized
}

type ErrForbidden struct{}

func (err ErrForbidden) Error() string {
	return lm.Forbidden
}

var (
	NotFound ErrNotFound = errors.New(lm.NotFound)
)

type ErrUnknown struct {
	code int
}

func (err ErrUnknown) Error() string {
	msg := fmt.Sprintf(lm.FailedGenericWithCode, err.code)
	return msg
}

// GenericErr returns an error appropriate to the given HTTP status (or actual error, if given).
func GenericErr(status int, err error) error {
	if err != nil {
		return err
	}
	switch status {
	case 200, 204, 201:
		return nil
	case 401, 400:
		return ErrUnauthorized{}
	case 404:
		return NotFound
	case 403:
		return ErrForbidden{}
	default:
		return ErrUnknown{code: status}
	}
}

type ConfigurableTransport interface {
	// SetTransport sets the http.Transport to use for requests. Can be used to set a proxy.
	SetTransport(t *http.Transport)
}

// Stripped down-ish version of rough http request function used in most of the API clients.
func Req(httpClient *http.Client, timeoutHandler TimeoutHandler, mode string, uri string, data any, queryParams url.Values, headers map[string]string, response bool) (string, int, error) {
	var params []byte
	if data != nil {
		params, _ = json.Marshal(data)
	}
	if qp := queryParams.Encode(); qp != "" {
		uri += "?" + qp
	}
	var req *http.Request
	if data != nil {
		req, _ = http.NewRequest(mode, uri, bytes.NewBuffer(params))
	} else {
		req, _ = http.NewRequest(mode, uri, nil)
	}
	req.Header.Add("Content-Type", "application/json")
	for name, value := range headers {
		req.Header.Add(name, value)
	}
	resp, err := httpClient.Do(req)
	if resp == nil {
		return "", 0, err
	}
	err = GenericErr(resp.StatusCode, err)
	if timeoutHandler != nil {
		defer timeoutHandler()
	}
	var responseText string
	defer resp.Body.Close()
	if response || err != nil {
		responseText, err = decodeResp(resp)
		if err != nil {
			return responseText, resp.StatusCode, err
		}
	}
	if err != nil {
		var msg any
		err = json.Unmarshal([]byte(responseText), &msg)
		if err != nil {
			return responseText, resp.StatusCode, err
		}
		if msg != nil {
			err = fmt.Errorf("got %d: %+v", resp.StatusCode, msg)
		}
		return responseText, resp.StatusCode, err
	}
	return responseText, resp.StatusCode, err
}

func decodeResp(resp *http.Response) (string, error) {
	var out io.Reader
	switch resp.Header.Get("Content-Encoding") {
	case "gzip":
		out, _ = gzip.NewReader(resp.Body)
	default:
		out = resp.Body
	}
	buf := new(strings.Builder)
	_, err := io.Copy(buf, out)
	if err != nil {
		return "", err
	}
	return buf.String(), nil
}