package authz

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

	"github.com/porter-dev/porter/api/server/authz/policy"
	"github.com/porter-dev/porter/api/server/shared/apierrors"
	"github.com/porter-dev/porter/api/server/shared/config"
	"github.com/porter-dev/porter/api/server/shared/requestutils"
	"github.com/porter-dev/porter/api/types"
	"github.com/porter-dev/porter/internal/models"
)

type PolicyMiddleware struct {
	config       *config.Config
	endpointMeta types.APIRequestMetadata
	loader       policy.PolicyDocumentLoader
}

func NewPolicyMiddleware(
	config *config.Config,
	endpointMeta types.APIRequestMetadata,
	loader policy.PolicyDocumentLoader,
) *PolicyMiddleware {
	return &PolicyMiddleware{config, endpointMeta, loader}
}

func (p *PolicyMiddleware) Middleware(next http.Handler) http.Handler {
	return &PolicyHandler{next, p.config, p.endpointMeta, p.loader}
}

type PolicyHandler struct {
	next         http.Handler
	config       *config.Config
	endpointMeta types.APIRequestMetadata
	loader       policy.PolicyDocumentLoader
}

func (h *PolicyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// get the full map of scopes to resource actions
	reqScopes, reqErr := getRequestActionForEndpoint(r, h.endpointMeta)

	if reqErr != nil {
		apierrors.HandleAPIError(h.config.Logger, h.config.Alerter, w, r, reqErr, true)
		return
	}

	policyLoaderOpts := &policy.PolicyLoaderOpts{}

	// first check if an api token exists in context
	if r.Context().Value("api_token") != nil {
		projID := reqScopes[types.ProjectScope].Resource.UInt

		// FIXME: find a clean way to get the project

		apiToken, _ := r.Context().Value("api_token").(*models.APIToken)
		policyLoaderOpts.ProjectToken = apiToken
		policyLoaderOpts.ProjectID = projID
	} else {
		projID := reqScopes[types.ProjectScope].Resource.UInt
		user, _ := r.Context().Value(types.UserScope).(*models.User)

		policyLoaderOpts.ProjectID = projID
		policyLoaderOpts.UserID = user.ID
	}

	// load policy documents for the user + project
	policyDocs, reqErr := h.loader.LoadPolicyDocuments(policyLoaderOpts)

	if reqErr != nil {
		apierrors.HandleAPIError(h.config.Logger, h.config.Alerter, w, r, reqErr, true)
		return
	}

	// validate that the policy permits the action
	hasAccess := policy.HasScopeAccess(policyDocs, reqScopes)

	if !hasAccess {
		apierrors.HandleAPIError(
			h.config.Logger,
			h.config.Alerter,
			w,
			r,
			apierrors.NewErrForbidden(fmt.Errorf("policy forbids action in project %d", policyLoaderOpts.ProjectID)),
			true,
		)

		return
	}

	// add the set of resource ids to the request context
	ctx := NewRequestScopeCtx(r.Context(), reqScopes)
	r = r.Clone(ctx)
	h.next.ServeHTTP(w, r)
}

func NewRequestScopeCtx(ctx context.Context, reqScopes map[types.PermissionScope]*types.RequestAction) context.Context {
	return context.WithValue(ctx, types.RequestScopeCtxKey, reqScopes)
}

func getRequestActionForEndpoint(
	r *http.Request,
	endpointMeta types.APIRequestMetadata,
) (res map[types.PermissionScope]*types.RequestAction, reqErr apierrors.RequestError) {
	res = make(map[types.PermissionScope]*types.RequestAction)

	// iterate through scopes, attach policies as needed
	for _, scope := range endpointMeta.Scopes {
		// find the resource ID and create the resource
		resource := types.NameOrUInt{}

		switch scope {
		case types.ProjectScope:
			resource.UInt, reqErr = requestutils.GetURLParamUint(r, types.URLParamProjectID)
		case types.ClusterScope:
			resource.UInt, reqErr = requestutils.GetURLParamUint(r, types.URLParamClusterID)
		case types.RegistryScope:
			resource.UInt, reqErr = requestutils.GetURLParamUint(r, types.URLParamRegistryID)
		case types.HelmRepoScope:
			resource.UInt, reqErr = requestutils.GetURLParamUint(r, types.URLParamHelmRepoID)
		case types.GitInstallationScope:
			resource.UInt, reqErr = requestutils.GetURLParamUint(r, types.URLParamGitInstallationID)
		case types.InfraScope:
			resource.UInt, reqErr = requestutils.GetURLParamUint(r, types.URLParamInfraID)
		case types.OperationScope:
			resource.Name, reqErr = requestutils.GetURLParamString(r, types.URLParamOperationID)
		case types.NamespaceScope:
			resource.Name, reqErr = requestutils.GetURLParamString(r, types.URLParamNamespace)
		case types.ReleaseScope:
			resource.Name, reqErr = requestutils.GetURLParamString(r, types.URLParamReleaseName)
		case types.StackScope:
			resource.Name, reqErr = requestutils.GetURLParamString(r, types.URLParamStackID)
		case types.InviteScope:
			resource.UInt, reqErr = requestutils.GetURLParamUint(r, types.URLParamInviteID)
		case types.GitlabIntegrationScope:
			resource.UInt, reqErr = requestutils.GetURLParamUint(r, types.URLParamIntegrationID)
		case types.APIContractRevisionScope:
			resource.Name, reqErr = requestutils.GetURLParamString(r, types.URLParamAPIContractRevisionID)
		}

		if reqErr != nil {
			return nil, reqErr
		}

		res[scope] = &types.RequestAction{
			Verb:     endpointMeta.Verb,
			Resource: resource,
		}
	}

	return res, nil
}
