package tools

import (
	"bytes"
	"compress/gzip"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"runtime/debug"
	"strings"
)

type APIError struct {
	Status  int    `json:"-"`
	Code    int    `json:"code"`
	Message string `json:"message"`
}

var (
	ERROR_GENERIC_SERVER             = APIError{Status: 500, Code: 10000, Message: "Server Error"}
	ERROR_GENERIC_NOT_FOUND          = APIError{Status: 404, Code: 10001, Message: "Endpoint Not Found"}
	ERROR_GENERIC_RATELIMIT          = APIError{Status: 429, Code: 10002, Message: "Too Many Requests"}
	ERROR_GENERIC_UNAUTHORIZED       = APIError{Status: 401, Code: 10003, Message: "Unauthorized"}
	ERROR_GENERIC_FORBIDDEN          = APIError{Status: 403, Code: 10004, Message: "Forbidden"}
	ERROR_GENERIC_METHOD_NOT_ALLOWED = APIError{Status: 405, Code: 10005, Message: "Method Not Allowed"}
	ERROR_SERVER_RESOURCES_EXHAUSTED = APIError{Status: 507, Code: 11006, Message: "Resources Exhausted"}
	ERROR_BODY_EMPTY                 = APIError{Status: 411, Code: 12000, Message: "Request Body is Empty"}
	ERROR_BODY_TOO_LARGE             = APIError{Status: 413, Code: 12001, Message: "Request Body is Too Large"}
	ERROR_BODY_INVALID_CONTENT_TYPE  = APIError{Status: 400, Code: 12002, Message: "Invalid 'Content-Type' Header"}
	ERROR_BODY_INVALID_CHALLENGE     = APIError{Status: 400, Code: 12003, Message: "Invalid 'X-Challenge-*' Header"}
	ERROR_BODY_INVALID_FIELD         = APIError{Status: 400, Code: 12004, Message: "Invalid Body Field"}
	ERROR_BODY_INVALID_DATA          = APIError{Status: 422, Code: 12005, Message: "Invalid Body"}
	ERROR_UNKNOWN_ENDPOINT           = APIError{Status: 404, Code: 13000, Message: "Unknown Endpoint"}
	ERROR_UNKNOWN_FUNCTION           = APIError{Status: 404, Code: 13001, Message: "Unknown Function"}
	ERROR_UNKNOWN_ANIMATION          = APIError{Status: 404, Code: 13002, Message: "Unknown Animation"}
	ERROR_UNKNOWN_TASK               = APIError{Status: 404, Code: 13003, Message: "Unknown Task"}
	ERROR_UNKNOWN_CHALLENGE          = APIError{Status: 404, Code: 13004, Message: "Unknown Challenge"}
	ERROR_CHALLENGE_INVALID          = APIError{Status: 400, Code: 14000, Message: "Invalid Challenge Result"}
	ERROR_CHALLENGE_TOO_EASY         = APIError{Status: 400, Code: 14001, Message: "Challenge Difficulty Too Low"}
	ERROR_CHALLENGE_EXPIRED          = APIError{Status: 400, Code: 14002, Message: "Challenge Expired"}
	ERROR_MEDIA_INVALID              = APIError{Status: 400, Code: 15000, Message: "Media Invalid"}
	ERROR_MEDIA_INAPPROPRIATE        = APIError{Status: 400, Code: 15001, Message: "Media Inappropriate"}
)

// Reject request due to a Client Mistake
func SendClientError(w http.ResponseWriter, r *http.Request, err APIError) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(err.Status)
	fmt.Fprintf(w, `{"code":%d,"message":%q}`, err.Code, err.Message)
}

// Reject request due to a Server Error
// Additionally collects debug information and logs it to the console
func SendServerError(w http.ResponseWriter, r *http.Request, err error) {

	debugStack := strings.Split(string(debug.Stack()), "\n")
	for i, item := range debugStack {
		debugStack[i] = strings.ReplaceAll(item, "\t", "    ")
	}
	if len(debugStack) > 5 {
		debugStack = debugStack[5:] // skip header
	}

	reqHeader := make(map[string]string, len(r.Header))
	for key, header := range r.Header {
		reqHeader[key] = strings.Join(header, ", ")
	}

	LoggerHTTP.Data(ERROR, err.Error(), map[string]any{
		"request": map[string]any{
			"method":  r.Method,
			"url":     r.URL.String(),
			"headers": reqHeader,
		},
		"error": map[string]any{
			"raw":     err,
			"message": err.Error(),
			"stack":   debugStack,
		},
	})

	if w != nil {
		SendClientError(w, r, ERROR_GENERIC_SERVER)
	}
}

// Respond to the request with a JSON object
func SendJSON(w http.ResponseWriter, r *http.Request, statusCode int, responseObject any) (int, error) {

	// Check Compression
	var g io.Writer
	if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
		w.Header().Set("Content-Encoding", "gzip")
		z := gzip.NewWriter(w)
		defer z.Close()
		g = z
	} else {
		g = w
	}

	// Stream Object
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(statusCode)
	if b, ok := responseObject.([]byte); ok {
		return g.Write(b)
	} else {
		j := json.NewEncoder(g)
		return 0, j.Encode(responseObject)
	}
}

// Encode Object as JSON and gzipped version
func PrepareStaticJSON(responseObject any) ([]byte, []byte, error) {

	// Encode Object
	buf, err := json.Marshal(responseObject)
	if err != nil {
		return nil, nil, err
	}

	// Compress Object
	cmp := bytes.Buffer{}
	zip := gzip.NewWriter(&cmp)

	if _, err := zip.Write(buf); err != nil {
		zip.Close()
		return nil, nil, err
	}
	if err := zip.Close(); err != nil {
		return nil, nil, err
	}

	return buf, cmp.Bytes(), nil
}

// Respond to the request with a Static JSON Object
func SendStaticJSON(w http.ResponseWriter, r *http.Request, statusCode int, content []byte, gzipped []byte) (int, error) {
	w.Header().Set("Content-Type", "application/json")
	if gzipped != nil && strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
		w.Header().Set("Content-Encoding", "gzip")
		return w.Write(gzipped)
	}
	return w.Write(content)
}
