package api

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

	"github.com/go-chi/chi/v5"

	"github.com/smallstep/certificates/acme"
	"github.com/smallstep/certificates/api/render"
	"github.com/smallstep/certificates/logging"
)

// NewAccountRequest represents the payload for a new account request.
type NewAccountRequest struct {
	Contact                []string                `json:"contact"`
	OnlyReturnExisting     bool                    `json:"onlyReturnExisting"`
	TermsOfServiceAgreed   bool                    `json:"termsOfServiceAgreed"`
	ExternalAccountBinding *ExternalAccountBinding `json:"externalAccountBinding,omitempty"`
}

func validateContacts(cs []string) error {
	for _, c := range cs {
		if c == "" {
			return acme.NewError(acme.ErrorMalformedType, "contact cannot be empty string")
		}
	}
	return nil
}

// Validate validates a new-account request body.
func (n *NewAccountRequest) Validate() error {
	if n.OnlyReturnExisting && len(n.Contact) > 0 {
		return acme.NewError(acme.ErrorMalformedType, "incompatible input; onlyReturnExisting must be alone")
	}
	return validateContacts(n.Contact)
}

// UpdateAccountRequest represents an update-account request.
type UpdateAccountRequest struct {
	Contact []string    `json:"contact"`
	Status  acme.Status `json:"status"`
}

// Validate validates a update-account request body.
func (u *UpdateAccountRequest) Validate() error {
	switch {
	case len(u.Status) > 0 && len(u.Contact) > 0:
		return acme.NewError(acme.ErrorMalformedType, "incompatible input; contact and "+
			"status updates are mutually exclusive")
	case len(u.Contact) > 0:
		if err := validateContacts(u.Contact); err != nil {
			return err
		}
		return nil
	case len(u.Status) > 0:
		if u.Status != acme.StatusDeactivated {
			return acme.NewError(acme.ErrorMalformedType, "cannot update account "+
				"status to %s, only deactivated", u.Status)
		}
		return nil
	default:
		// According to the ACME spec (https://tools.ietf.org/html/rfc8555#section-7.3.2)
		// accountUpdate should ignore any fields not recognized by the server.
		return nil
	}
}

// getAccountLocationPath returns the current account URL location.
// Returned location will be of the form: https://<ca-url>/acme/<provisioner>/account/<accID>
func getAccountLocationPath(ctx context.Context, linker acme.Linker, accID string) string {
	return linker.GetLink(ctx, acme.AccountLinkType, accID)
}

// NewAccount is the handler resource for creating new ACME accounts.
func NewAccount(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	db := acme.MustDatabaseFromContext(ctx)
	linker := acme.MustLinkerFromContext(ctx)

	payload, err := payloadFromContext(ctx)
	if err != nil {
		render.Error(w, r, err)
		return
	}
	var nar NewAccountRequest
	if err := json.Unmarshal(payload.value, &nar); err != nil {
		render.Error(w, r, acme.WrapError(acme.ErrorMalformedType, err,
			"failed to unmarshal new-account request payload"))
		return
	}
	if err := nar.Validate(); err != nil {
		render.Error(w, r, err)
		return
	}

	prov, err := acmeProvisionerFromContext(ctx)
	if err != nil {
		render.Error(w, r, err)
		return
	}

	httpStatus := http.StatusCreated
	acc, err := accountFromContext(ctx)
	if err != nil {
		var acmeErr *acme.Error
		if !errors.As(err, &acmeErr) || acmeErr.Status != http.StatusBadRequest {
			// Something went wrong ...
			render.Error(w, r, err)
			return
		}

		// Account does not exist //
		if nar.OnlyReturnExisting {
			render.Error(w, r, acme.NewError(acme.ErrorAccountDoesNotExistType,
				"account does not exist"))
			return
		}

		jwk, err := jwkFromContext(ctx)
		if err != nil {
			render.Error(w, r, err)
			return
		}

		eak, err := validateExternalAccountBinding(ctx, &nar)
		if err != nil {
			render.Error(w, r, err)
			return
		}

		acc = &acme.Account{
			Key:             jwk,
			Contact:         nar.Contact,
			Status:          acme.StatusValid,
			LocationPrefix:  getAccountLocationPath(ctx, linker, ""),
			ProvisionerID:   prov.ID,
			ProvisionerName: prov.Name,
		}
		if err := db.CreateAccount(ctx, acc); err != nil {
			render.Error(w, r, acme.WrapErrorISE(err, "error creating account"))
			return
		}

		if eak != nil { // means that we have a (valid) External Account Binding key that should be bound, updated and sent in the response
			if err := eak.BindTo(acc); err != nil {
				render.Error(w, r, err)
				return
			}
			if err := db.UpdateExternalAccountKey(ctx, prov.ID, eak); err != nil {
				render.Error(w, r, acme.WrapErrorISE(err, "error updating external account binding key"))
				return
			}
			acc.ExternalAccountBinding = nar.ExternalAccountBinding
		}
	} else {
		// Account exists
		httpStatus = http.StatusOK
	}

	linker.LinkAccount(ctx, acc)

	w.Header().Set("Location", getAccountLocationPath(ctx, linker, acc.ID))
	render.JSONStatus(w, r, acc, httpStatus)
}

// GetOrUpdateAccount is the api for updating an ACME account.
func GetOrUpdateAccount(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	db := acme.MustDatabaseFromContext(ctx)
	linker := acme.MustLinkerFromContext(ctx)

	acc, err := accountFromContext(ctx)
	if err != nil {
		render.Error(w, r, err)
		return
	}
	payload, err := payloadFromContext(ctx)
	if err != nil {
		render.Error(w, r, err)
		return
	}

	// If PostAsGet just respond with the account, otherwise process like a
	// normal Post request.
	if !payload.isPostAsGet {
		var uar UpdateAccountRequest
		if err := json.Unmarshal(payload.value, &uar); err != nil {
			render.Error(w, r, acme.WrapError(acme.ErrorMalformedType, err,
				"failed to unmarshal new-account request payload"))
			return
		}
		if err := uar.Validate(); err != nil {
			render.Error(w, r, err)
			return
		}
		if len(uar.Status) > 0 || len(uar.Contact) > 0 {
			if len(uar.Status) > 0 {
				acc.Status = uar.Status
			} else if len(uar.Contact) > 0 {
				acc.Contact = uar.Contact
			}

			if err := db.UpdateAccount(ctx, acc); err != nil {
				render.Error(w, r, acme.WrapErrorISE(err, "error updating account"))
				return
			}
		}
	}

	linker.LinkAccount(ctx, acc)

	w.Header().Set("Location", linker.GetLink(ctx, acme.AccountLinkType, acc.ID))
	render.JSON(w, r, acc)
}

func logOrdersByAccount(w http.ResponseWriter, oids []string) {
	if rl, ok := w.(logging.ResponseLogger); ok {
		m := map[string]interface{}{
			"orders": oids,
		}
		rl.WithFields(m)
	}
}

// GetOrdersByAccountID ACME api for retrieving the list of order urls belonging to an account.
func GetOrdersByAccountID(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	db := acme.MustDatabaseFromContext(ctx)
	linker := acme.MustLinkerFromContext(ctx)

	acc, err := accountFromContext(ctx)
	if err != nil {
		render.Error(w, r, err)
		return
	}
	accID := chi.URLParam(r, "accID")
	if acc.ID != accID {
		render.Error(w, r, acme.NewError(acme.ErrorUnauthorizedType, "account ID '%s' does not match url param '%s'", acc.ID, accID))
		return
	}

	orders, err := db.GetOrdersByAccountID(ctx, acc.ID)
	if err != nil {
		render.Error(w, r, err)
		return
	}

	linker.LinkOrdersByAccountID(ctx, orders)

	render.JSON(w, r, orders)
	logOrdersByAccount(w, orders)
}
