package api

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"strconv"

	"go.opentelemetry.io/otel"

	claims "github.com/grafana/authlib/types"

	"github.com/grafana/grafana/pkg/api/response"
	"github.com/grafana/grafana/pkg/api/routing"
	"github.com/grafana/grafana/pkg/middleware"
	"github.com/grafana/grafana/pkg/middleware/requestmeta"
	ac "github.com/grafana/grafana/pkg/services/accesscontrol"
	contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
	"github.com/grafana/grafana/pkg/services/user"
)

var tracer = otel.Tracer("github.com/grafana/grafana/pkg/services/accesscontrol/api")

func NewAccessControlAPI(router routing.RouteRegister, accesscontrol ac.AccessControl, service ac.Service, userSvc user.Service) *AccessControlAPI {
	return &AccessControlAPI{
		RouteRegister: router,
		Service:       service,
		userSvc:       userSvc,
		AccessControl: accesscontrol,
	}
}

type AccessControlAPI struct {
	Service       ac.Service
	AccessControl ac.AccessControl
	RouteRegister routing.RouteRegister
	userSvc       user.Service
}

func (api *AccessControlAPI) RegisterAPIEndpoints() {
	authorize := ac.Middleware(api.AccessControl)
	// Users
	api.RouteRegister.Group("/api/access-control", func(rr routing.RouteRegister) {
		rr.Get("/user/actions", middleware.ReqSignedIn, routing.Wrap(api.getUserActions))
		rr.Get("/user/permissions", middleware.ReqSignedIn, routing.Wrap(api.getUserPermissions))
		rr.Get("/users/permissions/search", authorize(ac.EvalPermission(ac.ActionUsersPermissionsRead)), routing.Wrap(api.searchUsersPermissions))
	}, requestmeta.SetOwner(requestmeta.TeamAuth))
}

// GET /api/access-control/user/actions
func (api *AccessControlAPI) getUserActions(c *contextmodel.ReqContext) response.Response {
	ctx, span := tracer.Start(c.Req.Context(), "accesscontrol.api.getUserActions")
	defer span.End()

	reloadCache := c.QueryBool("reloadcache")
	permissions, err := api.Service.GetUserPermissions(ctx, c.SignedInUser, ac.Options{ReloadCache: reloadCache})
	if err != nil {
		return response.JSON(http.StatusInternalServerError, err)
	}

	return response.JSON(http.StatusOK, ac.BuildPermissionsMap(permissions))
}

// GET /api/access-control/user/permissions
func (api *AccessControlAPI) getUserPermissions(c *contextmodel.ReqContext) response.Response {
	ctx, span := tracer.Start(c.Req.Context(), "accesscontrol.api.getUserPermissions")
	defer span.End()

	reloadCache := c.QueryBool("reloadcache")
	permissions, err := api.Service.GetUserPermissions(ctx, c.SignedInUser, ac.Options{ReloadCache: reloadCache})
	if err != nil {
		return response.JSON(http.StatusInternalServerError, err)
	}

	return response.JSON(http.StatusOK, ac.GroupScopesByActionContext(ctx, permissions))
}

// GET /api/access-control/users/permissions/search
func (api *AccessControlAPI) searchUsersPermissions(c *contextmodel.ReqContext) response.Response {
	ctx, span := tracer.Start(c.Req.Context(), "accesscontrol.api.searchUsersPermissions")
	defer span.End()

	searchOptions := ac.SearchOptions{
		ActionPrefix: c.Query("actionPrefix"),
		Action:       c.Query("action"),
		Scope:        c.Query("scope"),
	}

	// namespacedId is the typed identifier of an identity
	// it is specified using user/service account IDs or UIDs (ex: user:3, service-account:4, user:adisufjf93e9sd)
	if typedID := c.Query("namespacedId"); typedID != "" {
		userID, err := api.ComputeUserID(ctx, c.Query("namespacedId"))
		if err != nil {
			if errors.Is(err, user.ErrUserNotFound) {
				return response.JSON(http.StatusBadRequest, err.Error())
			}
			return response.JSON(http.StatusInternalServerError, err.Error())
		}

		searchOptions.UserID = userID
	}

	// Validate inputs
	if searchOptions.ActionPrefix != "" && searchOptions.Action != "" {
		return response.JSON(http.StatusBadRequest, "'action' and 'actionPrefix' are mutually exclusive")
	}

	if searchOptions.UserID <= 0 && searchOptions.ActionPrefix == "" && searchOptions.Action == "" {
		return response.JSON(http.StatusBadRequest, "at least one search option must be provided")
	}

	// Compute metadata
	permissions, err := api.Service.SearchUsersPermissions(ctx, c.SignedInUser, searchOptions)
	if err != nil {
		return response.Error(http.StatusInternalServerError, "could not get org user permissions", err)
	}

	permsByAction := map[int64]map[string][]string{}
	for userID, userPerms := range permissions {
		permsByAction[userID] = ac.Reduce(userPerms)
	}

	return response.JSON(http.StatusOK, permsByAction)
}

func (api *AccessControlAPI) ComputeUserID(ctx context.Context, typedID string) (int64, error) {
	if typedID == "" {
		return -1, nil
	}

	typ, idStr, err := claims.ParseTypeID(typedID)
	if err != nil {
		return 0, err
	}

	if !claims.IsIdentityType(typ, claims.TypeUser, claims.TypeServiceAccount) {
		return 0, fmt.Errorf("invalid type: %s", typ)
	}

	id, err := strconv.ParseInt(idStr, 10, 64)
	if err == nil {
		return id, nil
	}

	user, err := api.userSvc.GetByUID(ctx, &user.GetUserByUIDQuery{UID: idStr})
	if err != nil {
		return 0, err
	}

	return user.ID, nil
}
