package httputil

import (
	"errors"
	"fmt"
	"io"
	"net/http"
	"os"
	"runtime"
	"runtime/debug"
	"strings"
	"time"

	"github.com/kataras/iris/v12"
)

var validStackFuncs = []func(string) bool{
	func(file string) bool {
		return strings.Contains(file, "/mongodb/api/")
	},
}

// RuntimeCallerStack returns the app's `file:line` stacktrace
// to give more information about an error cause.
func RuntimeCallerStack() (s string) {
	var pcs [10]uintptr
	n := runtime.Callers(1, pcs[:])
	frames := runtime.CallersFrames(pcs[:n])

	for {
		frame, more := frames.Next()
		for _, fn := range validStackFuncs {
			if fn(frame.File) {
				s += fmt.Sprintf("\n\t\t\t%s:%d", frame.File, frame.Line)
			}
		}

		if !more {
			break
		}
	}

	return s
}

// HTTPError describes an HTTP error.
type HTTPError struct {
	error
	Stack       string    `json:"-"` // the whole stacktrace.
	CallerStack string    `json:"-"` // the caller, file:lineNumber
	When        time.Time `json:"-"` // the time that the error occurred.
	// ErrorCode int: maybe a collection of known error codes.
	StatusCode int `json:"statusCode"`
	// could be named as "reason" as well
	//  it's the message of the error.
	Description string `json:"description"`
}

func newError(statusCode int, err error, format string, args ...interface{}) HTTPError {
	if format == "" {
		format = http.StatusText(statusCode)
	}

	desc := fmt.Sprintf(format, args...)
	if err == nil {
		err = errors.New(desc)
	}

	return HTTPError{
		err,
		string(debug.Stack()),
		RuntimeCallerStack(),
		time.Now(),
		statusCode,
		desc,
	}
}

func (err HTTPError) writeHeaders(ctx iris.Context) {
	ctx.StatusCode(err.StatusCode)
	ctx.Header("X-Content-Type-Options", "nosniff")
}

// LogFailure will print out the failure to the "logger".
func LogFailure(logger io.Writer, ctx iris.Context, err HTTPError) {
	timeFmt := err.When.Format("2006/01/02 15:04:05")
	firstLine := fmt.Sprintf("%s %s: %s", timeFmt, http.StatusText(err.StatusCode), err.Error())
	whitespace := strings.Repeat(" ", len(timeFmt)+1)
	fmt.Fprintf(logger, "%s\n%sIP: %s\n%sURL: %s\n%sSource: %s\n",
		firstLine, whitespace, ctx.RemoteAddr(), whitespace, ctx.FullRequestURI(), whitespace, err.CallerStack)
}

// Fail will send the status code, write the error's reason
// and return the HTTPError for further use, i.e logging, see `InternalServerError`.
func Fail(ctx iris.Context, statusCode int, err error, format string, args ...interface{}) HTTPError {
	httpErr := newError(statusCode, err, format, args...)
	httpErr.writeHeaders(ctx)

	ctx.WriteString(httpErr.Description)
	return httpErr
}

// FailJSON will send to the client the error data as JSON.
// Useful for APIs.
func FailJSON(ctx iris.Context, statusCode int, err error, format string, args ...interface{}) HTTPError {
	httpErr := newError(statusCode, err, format, args...)
	httpErr.writeHeaders(ctx)

	ctx.JSON(httpErr)

	return httpErr
}

// InternalServerError logs to the server's terminal
// and dispatches to the client the 500 Internal Server Error.
// Internal Server errors are critical, so we log them to the `os.Stderr`.
func InternalServerError(ctx iris.Context, err error, format string, args ...interface{}) {
	LogFailure(os.Stderr, ctx, Fail(ctx, iris.StatusInternalServerError, err, format, args...))
}

// InternalServerErrorJSON acts exactly like `InternalServerError` but instead it sends the data as JSON.
// Useful for APIs.
func InternalServerErrorJSON(ctx iris.Context, err error, format string, args ...interface{}) {
	LogFailure(os.Stderr, ctx, FailJSON(ctx, iris.StatusInternalServerError, err, format, args...))
}

// UnauthorizedJSON sends JSON format of StatusUnauthorized(401) HTTPError value.
func UnauthorizedJSON(ctx iris.Context, err error, format string, args ...interface{}) HTTPError {
	return FailJSON(ctx, iris.StatusUnauthorized, err, format, args...)
}
