package api

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

	"go.step.sm/linkedca"

	"github.com/smallstep/certificates/acme"
	"github.com/smallstep/certificates/api/read"
	"github.com/smallstep/certificates/api/render"
	"github.com/smallstep/certificates/authority"
	"github.com/smallstep/certificates/authority/admin"
	"github.com/smallstep/certificates/authority/policy"
)

// PolicyAdminResponder is the interface responsible for writing ACME admin
// responses.
type PolicyAdminResponder interface {
	GetAuthorityPolicy(w http.ResponseWriter, r *http.Request)
	CreateAuthorityPolicy(w http.ResponseWriter, r *http.Request)
	UpdateAuthorityPolicy(w http.ResponseWriter, r *http.Request)
	DeleteAuthorityPolicy(w http.ResponseWriter, r *http.Request)
	GetProvisionerPolicy(w http.ResponseWriter, r *http.Request)
	CreateProvisionerPolicy(w http.ResponseWriter, r *http.Request)
	UpdateProvisionerPolicy(w http.ResponseWriter, r *http.Request)
	DeleteProvisionerPolicy(w http.ResponseWriter, r *http.Request)
	GetACMEAccountPolicy(w http.ResponseWriter, r *http.Request)
	CreateACMEAccountPolicy(w http.ResponseWriter, r *http.Request)
	UpdateACMEAccountPolicy(w http.ResponseWriter, r *http.Request)
	DeleteACMEAccountPolicy(w http.ResponseWriter, r *http.Request)
}

// policyAdminResponder implements PolicyAdminResponder.
type policyAdminResponder struct{}

// NewACMEAdminResponder returns a new PolicyAdminResponder.
func NewPolicyAdminResponder() PolicyAdminResponder {
	return &policyAdminResponder{}
}

// GetAuthorityPolicy handles the GET /admin/authority/policy request
func (par *policyAdminResponder) GetAuthorityPolicy(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	if err := blockLinkedCA(ctx); err != nil {
		render.Error(w, err)
		return
	}

	auth := mustAuthority(ctx)
	authorityPolicy, err := auth.GetAuthorityPolicy(r.Context())
	if ae, ok := err.(*admin.Error); ok && !ae.IsType(admin.ErrorNotFoundType) {
		render.Error(w, admin.WrapErrorISE(ae, "error retrieving authority policy"))
		return
	}

	if authorityPolicy == nil {
		render.Error(w, admin.NewError(admin.ErrorNotFoundType, "authority policy does not exist"))
		return
	}

	render.ProtoJSONStatus(w, authorityPolicy, http.StatusOK)
}

// CreateAuthorityPolicy handles the POST /admin/authority/policy request
func (par *policyAdminResponder) CreateAuthorityPolicy(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	if err := blockLinkedCA(ctx); err != nil {
		render.Error(w, err)
		return
	}

	auth := mustAuthority(ctx)
	authorityPolicy, err := auth.GetAuthorityPolicy(ctx)

	if ae, ok := err.(*admin.Error); ok && !ae.IsType(admin.ErrorNotFoundType) {
		render.Error(w, admin.WrapErrorISE(err, "error retrieving authority policy"))
		return
	}

	if authorityPolicy != nil {
		adminErr := admin.NewError(admin.ErrorConflictType, "authority already has a policy")
		render.Error(w, adminErr)
		return
	}

	var newPolicy = new(linkedca.Policy)
	if err := read.ProtoJSON(r.Body, newPolicy); err != nil {
		render.Error(w, err)
		return
	}

	newPolicy.Deduplicate()

	if err := validatePolicy(newPolicy); err != nil {
		render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error validating authority policy"))
		return
	}

	adm := linkedca.MustAdminFromContext(ctx)

	var createdPolicy *linkedca.Policy
	if createdPolicy, err = auth.CreateAuthorityPolicy(ctx, adm, newPolicy); err != nil {
		if isBadRequest(err) {
			render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error storing authority policy"))
			return
		}

		render.Error(w, admin.WrapErrorISE(err, "error storing authority policy"))
		return
	}

	render.ProtoJSONStatus(w, createdPolicy, http.StatusCreated)
}

// UpdateAuthorityPolicy handles the PUT /admin/authority/policy request
func (par *policyAdminResponder) UpdateAuthorityPolicy(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	if err := blockLinkedCA(ctx); err != nil {
		render.Error(w, err)
		return
	}

	auth := mustAuthority(ctx)
	authorityPolicy, err := auth.GetAuthorityPolicy(ctx)

	if ae, ok := err.(*admin.Error); ok && !ae.IsType(admin.ErrorNotFoundType) {
		render.Error(w, admin.WrapErrorISE(err, "error retrieving authority policy"))
		return
	}

	if authorityPolicy == nil {
		render.Error(w, admin.NewError(admin.ErrorNotFoundType, "authority policy does not exist"))
		return
	}

	var newPolicy = new(linkedca.Policy)
	if err := read.ProtoJSON(r.Body, newPolicy); err != nil {
		render.Error(w, err)
		return
	}

	newPolicy.Deduplicate()

	if err := validatePolicy(newPolicy); err != nil {
		render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error validating authority policy"))
		return
	}

	adm := linkedca.MustAdminFromContext(ctx)

	var updatedPolicy *linkedca.Policy
	if updatedPolicy, err = auth.UpdateAuthorityPolicy(ctx, adm, newPolicy); err != nil {
		if isBadRequest(err) {
			render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error updating authority policy"))
			return
		}

		render.Error(w, admin.WrapErrorISE(err, "error updating authority policy"))
		return
	}

	render.ProtoJSONStatus(w, updatedPolicy, http.StatusOK)
}

// DeleteAuthorityPolicy handles the DELETE /admin/authority/policy request
func (par *policyAdminResponder) DeleteAuthorityPolicy(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	if err := blockLinkedCA(ctx); err != nil {
		render.Error(w, err)
		return
	}

	auth := mustAuthority(ctx)
	authorityPolicy, err := auth.GetAuthorityPolicy(ctx)

	if ae, ok := err.(*admin.Error); ok && !ae.IsType(admin.ErrorNotFoundType) {
		render.Error(w, admin.WrapErrorISE(ae, "error retrieving authority policy"))
		return
	}

	if authorityPolicy == nil {
		render.Error(w, admin.NewError(admin.ErrorNotFoundType, "authority policy does not exist"))
		return
	}

	if err := auth.RemoveAuthorityPolicy(ctx); err != nil {
		render.Error(w, admin.WrapErrorISE(err, "error deleting authority policy"))
		return
	}

	render.JSONStatus(w, DeleteResponse{Status: "ok"}, http.StatusOK)
}

// GetProvisionerPolicy handles the GET /admin/provisioners/{name}/policy request
func (par *policyAdminResponder) GetProvisionerPolicy(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	if err := blockLinkedCA(ctx); err != nil {
		render.Error(w, err)
		return
	}

	prov := linkedca.MustProvisionerFromContext(ctx)
	provisionerPolicy := prov.GetPolicy()
	if provisionerPolicy == nil {
		render.Error(w, admin.NewError(admin.ErrorNotFoundType, "provisioner policy does not exist"))
		return
	}

	render.ProtoJSONStatus(w, provisionerPolicy, http.StatusOK)
}

// CreateProvisionerPolicy handles the POST /admin/provisioners/{name}/policy request
func (par *policyAdminResponder) CreateProvisionerPolicy(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	if err := blockLinkedCA(ctx); err != nil {
		render.Error(w, err)
		return
	}

	prov := linkedca.MustProvisionerFromContext(ctx)
	provisionerPolicy := prov.GetPolicy()
	if provisionerPolicy != nil {
		adminErr := admin.NewError(admin.ErrorConflictType, "provisioner %s already has a policy", prov.Name)
		render.Error(w, adminErr)
		return
	}

	var newPolicy = new(linkedca.Policy)
	if err := read.ProtoJSON(r.Body, newPolicy); err != nil {
		render.Error(w, err)
		return
	}

	newPolicy.Deduplicate()

	if err := validatePolicy(newPolicy); err != nil {
		render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error validating provisioner policy"))
		return
	}

	prov.Policy = newPolicy
	auth := mustAuthority(ctx)
	if err := auth.UpdateProvisioner(ctx, prov); err != nil {
		if isBadRequest(err) {
			render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error creating provisioner policy"))
			return
		}

		render.Error(w, admin.WrapErrorISE(err, "error creating provisioner policy"))
		return
	}

	render.ProtoJSONStatus(w, newPolicy, http.StatusCreated)
}

// UpdateProvisionerPolicy handles the PUT /admin/provisioners/{name}/policy request
func (par *policyAdminResponder) UpdateProvisionerPolicy(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	if err := blockLinkedCA(ctx); err != nil {
		render.Error(w, err)
		return
	}

	prov := linkedca.MustProvisionerFromContext(ctx)
	provisionerPolicy := prov.GetPolicy()
	if provisionerPolicy == nil {
		render.Error(w, admin.NewError(admin.ErrorNotFoundType, "provisioner policy does not exist"))
		return
	}

	var newPolicy = new(linkedca.Policy)
	if err := read.ProtoJSON(r.Body, newPolicy); err != nil {
		render.Error(w, err)
		return
	}

	newPolicy.Deduplicate()

	if err := validatePolicy(newPolicy); err != nil {
		render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error validating provisioner policy"))
		return
	}

	prov.Policy = newPolicy
	auth := mustAuthority(ctx)
	if err := auth.UpdateProvisioner(ctx, prov); err != nil {
		if isBadRequest(err) {
			render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error updating provisioner policy"))
			return
		}

		render.Error(w, admin.WrapErrorISE(err, "error updating provisioner policy"))
		return
	}

	render.ProtoJSONStatus(w, newPolicy, http.StatusOK)
}

// DeleteProvisionerPolicy handles the DELETE /admin/provisioners/{name}/policy request
func (par *policyAdminResponder) DeleteProvisionerPolicy(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	if err := blockLinkedCA(ctx); err != nil {
		render.Error(w, err)
		return
	}

	prov := linkedca.MustProvisionerFromContext(ctx)
	if prov.Policy == nil {
		render.Error(w, admin.NewError(admin.ErrorNotFoundType, "provisioner policy does not exist"))
		return
	}

	// remove the policy
	prov.Policy = nil

	auth := mustAuthority(ctx)
	if err := auth.UpdateProvisioner(ctx, prov); err != nil {
		render.Error(w, admin.WrapErrorISE(err, "error deleting provisioner policy"))
		return
	}

	render.JSONStatus(w, DeleteResponse{Status: "ok"}, http.StatusOK)
}

func (par *policyAdminResponder) GetACMEAccountPolicy(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	if err := blockLinkedCA(ctx); err != nil {
		render.Error(w, err)
		return
	}

	eak := linkedca.MustExternalAccountKeyFromContext(ctx)
	eakPolicy := eak.GetPolicy()
	if eakPolicy == nil {
		render.Error(w, admin.NewError(admin.ErrorNotFoundType, "ACME EAK policy does not exist"))
		return
	}

	render.ProtoJSONStatus(w, eakPolicy, http.StatusOK)
}

func (par *policyAdminResponder) CreateACMEAccountPolicy(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	if err := blockLinkedCA(ctx); err != nil {
		render.Error(w, err)
		return
	}

	prov := linkedca.MustProvisionerFromContext(ctx)
	eak := linkedca.MustExternalAccountKeyFromContext(ctx)
	eakPolicy := eak.GetPolicy()
	if eakPolicy != nil {
		adminErr := admin.NewError(admin.ErrorConflictType, "ACME EAK %s already has a policy", eak.Id)
		render.Error(w, adminErr)
		return
	}

	var newPolicy = new(linkedca.Policy)
	if err := read.ProtoJSON(r.Body, newPolicy); err != nil {
		render.Error(w, err)
		return
	}

	newPolicy.Deduplicate()

	if err := validatePolicy(newPolicy); err != nil {
		render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error validating ACME EAK policy"))
		return
	}

	eak.Policy = newPolicy

	acmeEAK := linkedEAKToCertificates(eak)
	acmeDB := acme.MustDatabaseFromContext(ctx)
	if err := acmeDB.UpdateExternalAccountKey(ctx, prov.GetId(), acmeEAK); err != nil {
		render.Error(w, admin.WrapErrorISE(err, "error creating ACME EAK policy"))
		return
	}

	render.ProtoJSONStatus(w, newPolicy, http.StatusCreated)
}

func (par *policyAdminResponder) UpdateACMEAccountPolicy(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	if err := blockLinkedCA(ctx); err != nil {
		render.Error(w, err)
		return
	}

	prov := linkedca.MustProvisionerFromContext(ctx)
	eak := linkedca.MustExternalAccountKeyFromContext(ctx)
	eakPolicy := eak.GetPolicy()
	if eakPolicy == nil {
		render.Error(w, admin.NewError(admin.ErrorNotFoundType, "ACME EAK policy does not exist"))
		return
	}

	var newPolicy = new(linkedca.Policy)
	if err := read.ProtoJSON(r.Body, newPolicy); err != nil {
		render.Error(w, err)
		return
	}

	newPolicy.Deduplicate()

	if err := validatePolicy(newPolicy); err != nil {
		render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error validating ACME EAK policy"))
		return
	}

	eak.Policy = newPolicy
	acmeEAK := linkedEAKToCertificates(eak)
	acmeDB := acme.MustDatabaseFromContext(ctx)
	if err := acmeDB.UpdateExternalAccountKey(ctx, prov.GetId(), acmeEAK); err != nil {
		render.Error(w, admin.WrapErrorISE(err, "error updating ACME EAK policy"))
		return
	}

	render.ProtoJSONStatus(w, newPolicy, http.StatusOK)
}

func (par *policyAdminResponder) DeleteACMEAccountPolicy(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	if err := blockLinkedCA(ctx); err != nil {
		render.Error(w, err)
		return
	}

	prov := linkedca.MustProvisionerFromContext(ctx)
	eak := linkedca.MustExternalAccountKeyFromContext(ctx)
	eakPolicy := eak.GetPolicy()
	if eakPolicy == nil {
		render.Error(w, admin.NewError(admin.ErrorNotFoundType, "ACME EAK policy does not exist"))
		return
	}

	// remove the policy
	eak.Policy = nil

	acmeEAK := linkedEAKToCertificates(eak)
	acmeDB := acme.MustDatabaseFromContext(ctx)
	if err := acmeDB.UpdateExternalAccountKey(ctx, prov.GetId(), acmeEAK); err != nil {
		render.Error(w, admin.WrapErrorISE(err, "error deleting ACME EAK policy"))
		return
	}

	render.JSONStatus(w, DeleteResponse{Status: "ok"}, http.StatusOK)
}

// blockLinkedCA blocks all API operations on linked deployments
func blockLinkedCA(ctx context.Context) error {
	// temporary blocking linked deployments
	adminDB := admin.MustFromContext(ctx)
	if a, ok := adminDB.(interface{ IsLinkedCA() bool }); ok && a.IsLinkedCA() {
		return admin.NewError(admin.ErrorNotImplementedType, "policy operations not yet supported in linked deployments")
	}
	return nil
}

// isBadRequest checks if an error should result in a bad request error
// returned to the client.
func isBadRequest(err error) bool {
	var pe *authority.PolicyError
	isPolicyError := errors.As(err, &pe)
	return isPolicyError && (pe.Typ == authority.AdminLockOut || pe.Typ == authority.EvaluationFailure || pe.Typ == authority.ConfigurationFailure)
}

func validatePolicy(p *linkedca.Policy) error {

	// convert the policy; return early if nil
	options := policy.LinkedToCertificates(p)
	if options == nil {
		return nil
	}

	var err error

	// Initialize a temporary x509 allow/deny policy engine
	if _, err = policy.NewX509PolicyEngine(options.GetX509Options()); err != nil {
		return err
	}

	// Initialize a temporary SSH allow/deny policy engine for host certificates
	if _, err = policy.NewSSHHostPolicyEngine(options.GetSSHOptions()); err != nil {
		return err
	}

	// Initialize a temporary SSH allow/deny policy engine for user certificates
	if _, err = policy.NewSSHUserPolicyEngine(options.GetSSHOptions()); err != nil {
		return err
	}

	return nil
}
