package api

import (
	"bytes"
	"errors"
	"fmt"
	"io"
	"net/http"
	"sort"
	"strconv"
	"strings"
	"sync"

	"github.com/gorilla/mux"

	"github.com/safing/portbase/database/record"
	"github.com/safing/portbase/formats/dsd"
	"github.com/safing/portbase/log"
	"github.com/safing/portbase/modules"
)

// Endpoint describes an API Endpoint.
// Path and at least one permission are required.
// As is exactly one function.
type Endpoint struct { //nolint:maligned
	// Name is the human reabable name of the endpoint.
	Name string
	// Description is the human readable description and documentation of the endpoint.
	Description string
	// Parameters is the parameter documentation.
	Parameters []Parameter `json:",omitempty"`

	// Path describes the URL path of the endpoint.
	Path string

	// MimeType defines the content type of the returned data.
	MimeType string

	// Read defines the required read permission.
	Read Permission `json:",omitempty"`

	// ReadMethod sets the required read method for the endpoint.
	// Available methods are:
	// GET: Returns data only, no action is taken, nothing is changed.
	// If omitted, defaults to GET.
	//
	// This field is currently being introduced and will only warn and not deny
	// access if the write method does not match.
	ReadMethod string `json:",omitempty"`

	// Write defines the required write permission.
	Write Permission `json:",omitempty"`

	// WriteMethod sets the required write method for the endpoint.
	// Available methods are:
	// POST: Create a new resource; Change a status; Execute a function
	// PUT: Update an existing resource
	// DELETE: Remove an existing resource
	// If omitted, defaults to POST.
	//
	// This field is currently being introduced and will only warn and not deny
	// access if the write method does not match.
	WriteMethod string `json:",omitempty"`

	// BelongsTo defines which module this endpoint belongs to.
	// The endpoint will not be accessible if the module is not online.
	BelongsTo *modules.Module `json:"-"`

	// ActionFunc is for simple actions with a return message for the user.
	ActionFunc ActionFunc `json:"-"`

	// DataFunc is for returning raw data that the caller for further processing.
	DataFunc DataFunc `json:"-"`

	// StructFunc is for returning any kind of struct.
	StructFunc StructFunc `json:"-"`

	// RecordFunc is for returning a database record. It will be properly locked
	// and marshalled including metadata.
	RecordFunc RecordFunc `json:"-"`

	// HandlerFunc is the raw http handler.
	HandlerFunc http.HandlerFunc `json:"-"`
}

// Parameter describes a parameterized variation of an endpoint.
type Parameter struct {
	Method      string
	Field       string
	Value       string
	Description string
}

// HTTPStatusProvider is an interface for errors to provide a custom HTTP
// status code.
type HTTPStatusProvider interface {
	HTTPStatus() int
}

// HTTPStatusError represents an error with an HTTP status code.
type HTTPStatusError struct {
	err  error
	code int
}

// Error returns the error message.
func (e *HTTPStatusError) Error() string {
	return e.err.Error()
}

// Unwrap return the wrapped error.
func (e *HTTPStatusError) Unwrap() error {
	return e.err
}

// HTTPStatus returns the HTTP status code this error.
func (e *HTTPStatusError) HTTPStatus() int {
	return e.code
}

// ErrorWithStatus adds the HTTP status code to the error.
func ErrorWithStatus(err error, code int) error {
	return &HTTPStatusError{
		err:  err,
		code: code,
	}
}

type (
	// ActionFunc is for simple actions with a return message for the user.
	ActionFunc func(ar *Request) (msg string, err error)

	// DataFunc is for returning raw data that the caller for further processing.
	DataFunc func(ar *Request) (data []byte, err error)

	// StructFunc is for returning any kind of struct.
	StructFunc func(ar *Request) (i interface{}, err error)

	// RecordFunc is for returning a database record. It will be properly locked
	// and marshalled including metadata.
	RecordFunc func(ar *Request) (r record.Record, err error)
)

// MIME Types.
const (
	MimeTypeJSON string = "application/json"
	MimeTypeText string = "text/plain"

	apiV1Path = "/api/v1/"
)

func init() {
	RegisterHandler(apiV1Path+"{endpointPath:.+}", &endpointHandler{})
}

var (
	endpoints     = make(map[string]*Endpoint)
	endpointsMux  = mux.NewRouter()
	endpointsLock sync.RWMutex

	// ErrInvalidEndpoint is returned when an invalid endpoint is registered.
	ErrInvalidEndpoint = errors.New("endpoint is invalid")

	// ErrAlreadyRegistered is returned when there already is an endpoint with
	// the same path registered.
	ErrAlreadyRegistered = errors.New("an endpoint for this path is already registered")
)

func getAPIContext(r *http.Request) (apiEndpoint *Endpoint, apiRequest *Request) {
	// Get request context and check if we already have an action cached.
	apiRequest = GetAPIRequest(r)
	if apiRequest == nil {
		return nil, nil
	}
	var ok bool
	apiEndpoint, ok = apiRequest.HandlerCache.(*Endpoint)
	if ok {
		return apiEndpoint, apiRequest
	}

	endpointsLock.RLock()
	defer endpointsLock.RUnlock()

	// Get handler for request.
	// Gorilla does not support handling this on our own very well.
	// See github.com/gorilla/mux.ServeHTTP for reference.
	var match mux.RouteMatch
	var handler http.Handler
	if endpointsMux.Match(r, &match) {
		handler = match.Handler
		apiRequest.Route = match.Route
		// Add/Override variables instead of replacing.
		for k, v := range match.Vars {
			apiRequest.URLVars[k] = v
		}
	} else {
		return nil, apiRequest
	}

	apiEndpoint, ok = handler.(*Endpoint)
	if ok {
		// Cache for next operation.
		apiRequest.HandlerCache = apiEndpoint
	}
	return apiEndpoint, apiRequest
}

// RegisterEndpoint registers a new endpoint. An error will be returned if it
// does not pass the sanity checks.
func RegisterEndpoint(e Endpoint) error {
	if err := e.check(); err != nil {
		return fmt.Errorf("%w: %w", ErrInvalidEndpoint, err)
	}

	endpointsLock.Lock()
	defer endpointsLock.Unlock()

	_, ok := endpoints[e.Path]
	if ok {
		return ErrAlreadyRegistered
	}

	endpoints[e.Path] = &e
	endpointsMux.Handle(apiV1Path+e.Path, &e)
	return nil
}

// GetEndpointByPath returns the endpoint registered with the given path.
func GetEndpointByPath(path string) (*Endpoint, error) {
	endpointsLock.Lock()
	defer endpointsLock.Unlock()
	endpoint, ok := endpoints[path]
	if !ok {
		return nil, fmt.Errorf("no registered endpoint on path: %q", path)
	}

	return endpoint, nil
}

func (e *Endpoint) check() error {
	// Check path.
	if strings.TrimSpace(e.Path) == "" {
		return errors.New("path is missing")
	}

	// Check permissions.
	if e.Read < Dynamic || e.Read > PermitSelf {
		return errors.New("invalid read permission")
	}
	if e.Write < Dynamic || e.Write > PermitSelf {
		return errors.New("invalid write permission")
	}

	// Check methods.
	if e.Read != NotSupported {
		switch e.ReadMethod {
		case http.MethodGet:
			// All good.
		case "":
			// Set to default.
			e.ReadMethod = http.MethodGet
		default:
			return errors.New("invalid read method")
		}
	} else {
		e.ReadMethod = ""
	}
	if e.Write != NotSupported {
		switch e.WriteMethod {
		case http.MethodPost,
			http.MethodPut,
			http.MethodDelete:
			// All good.
		case "":
			// Set to default.
			e.WriteMethod = http.MethodPost
		default:
			return errors.New("invalid write method")
		}
	} else {
		e.WriteMethod = ""
	}

	// Check functions.
	var defaultMimeType string
	fnCnt := 0
	if e.ActionFunc != nil {
		fnCnt++
		defaultMimeType = MimeTypeText
	}
	if e.DataFunc != nil {
		fnCnt++
		defaultMimeType = MimeTypeText
	}
	if e.StructFunc != nil {
		fnCnt++
		defaultMimeType = MimeTypeJSON
	}
	if e.RecordFunc != nil {
		fnCnt++
		defaultMimeType = MimeTypeJSON
	}
	if e.HandlerFunc != nil {
		fnCnt++
		defaultMimeType = MimeTypeText
	}
	if fnCnt != 1 {
		return errors.New("only one function may be set")
	}

	// Set default mime type.
	if e.MimeType == "" {
		e.MimeType = defaultMimeType
	}

	return nil
}

// ExportEndpoints exports the registered endpoints. The returned data must be
// treated as immutable.
func ExportEndpoints() []*Endpoint {
	endpointsLock.RLock()
	defer endpointsLock.RUnlock()

	// Copy the map into a slice.
	eps := make([]*Endpoint, 0, len(endpoints))
	for _, ep := range endpoints {
		eps = append(eps, ep)
	}

	sort.Sort(sortByPath(eps))
	return eps
}

type sortByPath []*Endpoint

func (eps sortByPath) Len() int           { return len(eps) }
func (eps sortByPath) Less(i, j int) bool { return eps[i].Path < eps[j].Path }
func (eps sortByPath) Swap(i, j int)      { eps[i], eps[j] = eps[j], eps[i] }

type endpointHandler struct{}

var _ AuthenticatedHandler = &endpointHandler{} // Compile time interface check.

// ReadPermission returns the read permission for the handler.
func (eh *endpointHandler) ReadPermission(r *http.Request) Permission {
	apiEndpoint, _ := getAPIContext(r)
	if apiEndpoint != nil {
		return apiEndpoint.Read
	}
	return NotFound
}

// WritePermission returns the write permission for the handler.
func (eh *endpointHandler) WritePermission(r *http.Request) Permission {
	apiEndpoint, _ := getAPIContext(r)
	if apiEndpoint != nil {
		return apiEndpoint.Write
	}
	return NotFound
}

// ServeHTTP handles the http request.
func (eh *endpointHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	apiEndpoint, apiRequest := getAPIContext(r)
	if apiEndpoint == nil || apiRequest == nil {
		http.NotFound(w, r)
		return
	}

	apiEndpoint.ServeHTTP(w, r)
}

// ServeHTTP handles the http request.
func (e *Endpoint) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	_, apiRequest := getAPIContext(r)
	if apiRequest == nil {
		http.NotFound(w, r)
		return
	}

	// Wait for the owning module to be ready.
	if !moduleIsReady(e.BelongsTo) {
		http.Error(w, "The API endpoint is not ready yet or the its module is not enabled. Reload (F5) to try again.", http.StatusServiceUnavailable)
		return
	}

	// Return OPTIONS request before starting to handle normal requests.
	if r.Method == http.MethodOptions {
		w.WriteHeader(http.StatusNoContent)
		return
	}

	eMethod, readMethod, ok := getEffectiveMethod(r)
	if !ok {
		http.Error(w, "unsupported method for the actions API", http.StatusMethodNotAllowed)
		return
	}

	if readMethod {
		if eMethod != e.ReadMethod {
			log.Tracer(r.Context()).Warningf(
				"api: method %q does not match required read method %q%s",
				r.Method,
				e.ReadMethod,
				" - this will be an error and abort the request in the future",
			)
		}
	} else {
		if eMethod != e.WriteMethod {
			log.Tracer(r.Context()).Warningf(
				"api: method %q does not match required write method %q%s",
				r.Method,
				e.WriteMethod,
				" - this will be an error and abort the request in the future",
			)
		}
	}

	switch eMethod {
	case http.MethodGet, http.MethodDelete:
		// Nothing to do for these.
	case http.MethodPost, http.MethodPut:
		// Read body data.
		inputData, ok := readBody(w, r)
		if !ok {
			return
		}
		apiRequest.InputData = inputData

		// restore request body for any http.HandlerFunc below
		r.Body = io.NopCloser(bytes.NewReader(inputData))
	default:
		// Defensive.
		http.Error(w, "unsupported method for the actions API", http.StatusMethodNotAllowed)
		return
	}

	// Add response headers to request struct so that the endpoint can work with them.
	apiRequest.ResponseHeader = w.Header()

	// Execute action function and get response data
	var responseData []byte
	var err error

	switch {
	case e.ActionFunc != nil:
		var msg string
		msg, err = e.ActionFunc(apiRequest)
		if !strings.HasSuffix(msg, "\n") {
			msg += "\n"
		}
		if err == nil {
			responseData = []byte(msg)
		}

	case e.DataFunc != nil:
		responseData, err = e.DataFunc(apiRequest)

	case e.StructFunc != nil:
		var v interface{}
		v, err = e.StructFunc(apiRequest)
		if err == nil && v != nil {
			var mimeType string
			responseData, mimeType, _, err = dsd.MimeDump(v, r.Header.Get("Accept"))
			if err == nil {
				w.Header().Set("Content-Type", mimeType)
			}
		}

	case e.RecordFunc != nil:
		var rec record.Record
		rec, err = e.RecordFunc(apiRequest)
		if err == nil && r != nil {
			responseData, err = MarshalRecord(rec, false)
		}

	case e.HandlerFunc != nil:
		e.HandlerFunc(w, r)
		return

	default:
		http.Error(w, "missing handler", http.StatusInternalServerError)
		return
	}

	// Check for handler error.
	if err != nil {
		var statusProvider HTTPStatusProvider
		if errors.As(err, &statusProvider) {
			http.Error(w, err.Error(), statusProvider.HTTPStatus())
		} else {
			http.Error(w, err.Error(), http.StatusInternalServerError)
		}
		return
	}

	// Return no content if there is none, or if request is HEAD.
	if len(responseData) == 0 || r.Method == http.MethodHead {
		w.WriteHeader(http.StatusNoContent)
		return
	}

	// Set content type if not yet set.
	if w.Header().Get("Content-Type") == "" {
		w.Header().Set("Content-Type", e.MimeType+"; charset=utf-8")
	}

	// Write response.
	w.Header().Set("Content-Length", strconv.Itoa(len(responseData)))
	w.WriteHeader(http.StatusOK)
	_, err = w.Write(responseData)
	if err != nil {
		log.Tracer(r.Context()).Warningf("api: failed to write response: %s", err)
	}
}

func readBody(w http.ResponseWriter, r *http.Request) (inputData []byte, ok bool) {
	// Check for too long content in order to prevent death.
	if r.ContentLength > 20000000 { // 20MB
		http.Error(w, "too much input data", http.StatusRequestEntityTooLarge)
		return nil, false
	}

	// Read and close body.
	inputData, err := io.ReadAll(r.Body)
	if err != nil {
		http.Error(w, "failed to read body"+err.Error(), http.StatusInternalServerError)
		return nil, false
	}
	return inputData, true
}
