package api

import (
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"runtime/debug"

	"github.com/gorilla/mux"
	"github.com/mattermost/focalboard/server/app"
	"github.com/mattermost/focalboard/server/model"
	"github.com/mattermost/focalboard/server/services/audit"
	"github.com/mattermost/focalboard/server/services/permissions"

	"github.com/mattermost/mattermost/server/public/shared/mlog"
)

const (
	HeaderRequestedWith    = "X-Requested-With"
	HeaderRequestedWithXML = "XMLHttpRequest"
	UploadFormFileKey      = "file"
	True                   = "true"

	ErrorNoTeamCode    = 1000
	ErrorNoTeamMessage = "No team"
)

var (
	ErrHandlerPanic = errors.New("http handler panic")
)

// ----------------------------------------------------------------------------------------------------
// REST APIs

type API struct {
	app             *app.App
	authService     string
	permissions     permissions.PermissionsService
	singleUserToken string
	MattermostAuth  bool
	logger          mlog.LoggerIFace
	audit           *audit.Audit
	isPlugin        bool
}

func NewAPI(
	app *app.App,
	singleUserToken string,
	authService string,
	permissions permissions.PermissionsService,
	logger mlog.LoggerIFace,
	audit *audit.Audit,
	isPlugin bool,
) *API {
	return &API{
		app:             app,
		singleUserToken: singleUserToken,
		authService:     authService,
		permissions:     permissions,
		logger:          logger,
		audit:           audit,
		isPlugin:        isPlugin,
	}
}

func (a *API) RegisterRoutes(r *mux.Router) {
	apiv2 := r.PathPrefix("/api/v2").Subrouter()
	apiv2.Use(a.panicHandler)
	apiv2.Use(a.requireCSRFToken)

	/* ToDo:
	apiv3 := r.PathPrefix("/api/v3").Subrouter()
	apiv3.Use(a.panicHandler)
	apiv3.Use(a.requireCSRFToken)
	*/

	// V2 routes (ToDo: migrate these to V3 when ready to ship V3)
	a.registerUsersRoutes(apiv2)
	a.registerAuthRoutes(apiv2)
	a.registerMembersRoutes(apiv2)
	a.registerCategoriesRoutes(apiv2)
	a.registerSharingRoutes(apiv2)
	a.registerTeamsRoutes(apiv2)
	a.registerAchivesRoutes(apiv2)
	a.registerSubscriptionsRoutes(apiv2)
	a.registerFilesRoutes(apiv2)
	a.registerOnboardingRoutes(apiv2)
	a.registerSearchRoutes(apiv2)
	a.registerConfigRoutes(apiv2)
	a.registerBoardsAndBlocksRoutes(apiv2)
	a.registerChannelsRoutes(apiv2)
	a.registerTemplatesRoutes(apiv2)
	a.registerBoardsRoutes(apiv2)
	a.registerBlocksRoutes(apiv2)
	a.registerContentBlocksRoutes(apiv2)
	a.registerStatisticsRoutes(apiv2)
	a.registerComplianceRoutes(apiv2)

	// V3 routes
	a.registerCardsRoutes(apiv2)

	// System routes are outside the /api/v2 path
	a.registerSystemRoutes(r)
}

func (a *API) RegisterAdminRoutes(r *mux.Router) {
	r.HandleFunc("/api/v2/admin/users/{username}/password", a.adminRequired(a.handleAdminSetPassword)).Methods("POST")
}

func getUserID(r *http.Request) string {
	ctx := r.Context()
	session, ok := ctx.Value(sessionContextKey).(*model.Session)
	if !ok {
		return ""
	}
	return session.UserID
}

func (a *API) panicHandler(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer func() {
			if p := recover(); p != nil {
				a.logger.Error("Http handler panic",
					mlog.Any("panic", p),
					mlog.String("stack", string(debug.Stack())),
					mlog.String("uri", r.URL.Path),
				)
				a.errorResponse(w, r, ErrHandlerPanic)
			}
		}()

		next.ServeHTTP(w, r)
	})
}

func (a *API) requireCSRFToken(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if !a.checkCSRFToken(r) {
			a.logger.Error("checkCSRFToken FAILED")
			a.errorResponse(w, r, model.NewErrBadRequest("checkCSRFToken FAILED"))
			return
		}

		next.ServeHTTP(w, r)
	})
}

func (a *API) checkCSRFToken(r *http.Request) bool {
	token := r.Header.Get(HeaderRequestedWith)
	return token == HeaderRequestedWithXML
}

func (a *API) hasValidReadTokenForBoard(r *http.Request, boardID string) bool {
	query := r.URL.Query()
	readToken := query.Get("read_token")

	if len(readToken) < 1 {
		return false
	}

	isValid, err := a.app.IsValidReadToken(boardID, readToken)
	if err != nil {
		a.logger.Error("IsValidReadTokenForBoard ERROR", mlog.Err(err))
		return false
	}

	return isValid
}

func (a *API) userIsGuest(userID string) (bool, error) {
	if a.singleUserToken != "" {
		return false, nil
	}
	return a.app.UserIsGuest(userID)
}

// Response helpers

func (a *API) errorResponse(w http.ResponseWriter, r *http.Request, err error) {
	a.logger.Error(err.Error())
	errorResponse := model.ErrorResponse{Error: err.Error()}

	switch {
	case model.IsErrBadRequest(err):
		errorResponse.ErrorCode = http.StatusBadRequest
	case model.IsErrUnauthorized(err):
		errorResponse.ErrorCode = http.StatusUnauthorized
	case model.IsErrForbidden(err):
		errorResponse.ErrorCode = http.StatusForbidden
	case model.IsErrNotFound(err):
		errorResponse.ErrorCode = http.StatusNotFound
	case model.IsErrRequestEntityTooLarge(err):
		errorResponse.ErrorCode = http.StatusRequestEntityTooLarge
	case model.IsErrNotImplemented(err):
		errorResponse.ErrorCode = http.StatusNotImplemented
	default:
		a.logger.Error("API ERROR",
			mlog.Int("code", http.StatusInternalServerError),
			mlog.Err(err),
			mlog.String("api", r.URL.Path),
		)
		errorResponse.Error = "internal server error"
		errorResponse.ErrorCode = http.StatusInternalServerError
	}

	setResponseHeader(w, "Content-Type", "application/json")
	data, err := json.Marshal(errorResponse)
	if err != nil {
		data = []byte("{}")
	}

	w.WriteHeader(errorResponse.ErrorCode)
	_, _ = w.Write(data)
}

func stringResponse(w http.ResponseWriter, message string) {
	setResponseHeader(w, "Content-Type", "text/plain")
	_, _ = fmt.Fprint(w, message)
}

func jsonStringResponse(w http.ResponseWriter, code int, message string) { //nolint:unparam
	setResponseHeader(w, "Content-Type", "application/json")
	w.WriteHeader(code)
	fmt.Fprint(w, message)
}

func jsonBytesResponse(w http.ResponseWriter, code int, json []byte) { //nolint:unparam
	setResponseHeader(w, "Content-Type", "application/json")
	w.WriteHeader(code)
	_, _ = w.Write(json)
}

func setResponseHeader(w http.ResponseWriter, key string, value string) { //nolint:unparam
	header := w.Header()
	if header == nil {
		return
	}
	header.Set(key, value)
}
