package api

import (
	"context"
	"crypto/x509"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"net"
	"net/http"
	"strings"
	"time"

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

	"go.step.sm/crypto/randutil"
	"go.step.sm/crypto/x509util"

	"github.com/smallstep/certificates/acme"
	"github.com/smallstep/certificates/acme/wire"
	"github.com/smallstep/certificates/api/render"
	"github.com/smallstep/certificates/authority/policy"
	"github.com/smallstep/certificates/authority/provisioner"
)

// NewOrderRequest represents the body for a NewOrder request.
type NewOrderRequest struct {
	Identifiers []acme.Identifier `json:"identifiers"`
	NotBefore   time.Time         `json:"notBefore,omitempty"`
	NotAfter    time.Time         `json:"notAfter,omitempty"`
}

// Validate validates a new-order request body.
func (n *NewOrderRequest) Validate() error {
	if len(n.Identifiers) == 0 {
		return acme.NewError(acme.ErrorMalformedType, "identifiers list cannot be empty")
	}
	for _, id := range n.Identifiers {
		switch id.Type {
		case acme.IP:
			if net.ParseIP(id.Value) == nil {
				return acme.NewError(acme.ErrorMalformedType, "invalid IP address: %s", id.Value)
			}
		case acme.DNS:
			value, _ := trimIfWildcard(id.Value)
			if _, err := x509util.SanitizeName(value); err != nil {
				return acme.NewError(acme.ErrorMalformedType, "invalid DNS name: %s", id.Value)
			}
		case acme.PermanentIdentifier:
			if id.Value == "" {
				return acme.NewError(acme.ErrorMalformedType, "permanent identifier cannot be empty")
			}
		case acme.WireUser, acme.WireDevice:
			// validation of Wire identifiers is performed in `validateWireIdentifiers`, but
			// marked here as known and supported types.
			continue
		default:
			return acme.NewError(acme.ErrorMalformedType, "identifier type unsupported: %s", id.Type)
		}
	}

	if err := n.validateWireIdentifiers(); err != nil {
		return acme.WrapError(acme.ErrorMalformedType, err, "failed validating Wire identifiers")
	}

	// TODO(hs): add some validations for DNS domains?
	// TODO(hs): combine the errors from this with allow/deny policy, like example error in https://datatracker.ietf.org/doc/html/rfc8555#section-6.7.1

	return nil
}

func (n *NewOrderRequest) validateWireIdentifiers() error {
	if !n.hasWireIdentifiers() {
		return nil
	}

	userIdentifiers := identifiersOfType(acme.WireUser, n.Identifiers)
	deviceIdentifiers := identifiersOfType(acme.WireDevice, n.Identifiers)

	if len(userIdentifiers) != 1 {
		return fmt.Errorf("expected exactly one Wire UserID identifier; got %d", len(userIdentifiers))
	}
	if len(deviceIdentifiers) != 1 {
		return fmt.Errorf("expected exactly one Wire DeviceID identifier, got %d", len(deviceIdentifiers))
	}

	wireUserID, err := wire.ParseUserID(userIdentifiers[0].Value)
	if err != nil {
		return fmt.Errorf("failed parsing Wire UserID: %w", err)
	}

	wireDeviceID, err := wire.ParseDeviceID(deviceIdentifiers[0].Value)
	if err != nil {
		return fmt.Errorf("failed parsing Wire DeviceID: %w", err)
	}
	if _, err := wire.ParseClientID(wireDeviceID.ClientID); err != nil {
		return fmt.Errorf("invalid Wire client ID %q: %w", wireDeviceID.ClientID, err)
	}

	switch {
	case wireUserID.Domain != wireDeviceID.Domain:
		return fmt.Errorf("UserID domain %q does not match DeviceID domain %q", wireUserID.Domain, wireDeviceID.Domain)
	case wireUserID.Name != wireDeviceID.Name:
		return fmt.Errorf("UserID name %q does not match DeviceID name %q", wireUserID.Name, wireDeviceID.Name)
	case wireUserID.Handle != wireDeviceID.Handle:
		return fmt.Errorf("UserID handle %q does not match DeviceID handle %q", wireUserID.Handle, wireDeviceID.Handle)
	}

	return nil
}

// hasWireIdentifiers returns whether the [NewOrderRequest] contains
// Wire identifiers.
func (n *NewOrderRequest) hasWireIdentifiers() bool {
	for _, i := range n.Identifiers {
		if i.Type == acme.WireUser || i.Type == acme.WireDevice {
			return true
		}
	}
	return false
}

// identifiersOfType returns the Identifiers that are of type typ.
func identifiersOfType(typ acme.IdentifierType, ids []acme.Identifier) (result []acme.Identifier) {
	for _, id := range ids {
		if id.Type == typ {
			result = append(result, id)
		}
	}
	return
}

// FinalizeRequest captures the body for a Finalize order request.
type FinalizeRequest struct {
	CSR string `json:"csr"`
	csr *x509.CertificateRequest
}

// Validate validates a finalize request body.
func (f *FinalizeRequest) Validate() error {
	var err error
	// RFC 8555 isn't 100% conclusive about using raw base64-url encoding for the
	// CSR specifically, instead of "normal" base64-url encoding (incl. padding).
	// By trimming the padding from CSRs submitted by ACME clients that use
	// base64-url encoding instead of raw base64-url encoding, these are also
	// supported. This was reported in https://github.com/smallstep/certificates/issues/939
	// to be the case for a Synology DSM NAS system.
	csrBytes, err := base64.RawURLEncoding.DecodeString(strings.TrimRight(f.CSR, "="))
	if err != nil {
		return acme.WrapError(acme.ErrorMalformedType, err, "error base64url decoding csr")
	}
	f.csr, err = x509.ParseCertificateRequest(csrBytes)
	if err != nil {
		return acme.WrapError(acme.ErrorMalformedType, err, "unable to parse csr")
	}
	if err = f.csr.CheckSignature(); err != nil {
		return acme.WrapError(acme.ErrorMalformedType, err, "csr failed signature check")
	}
	return nil
}

var defaultOrderExpiry = time.Hour * 24
var defaultOrderBackdate = time.Minute

// NewOrder ACME api for creating a new order.
func NewOrder(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	ca := mustAuthority(ctx)
	db := acme.MustDatabaseFromContext(ctx)
	linker := acme.MustLinkerFromContext(ctx)

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

	var nor NewOrderRequest
	if err := json.Unmarshal(payload.value, &nor); err != nil {
		render.Error(w, r, acme.WrapError(acme.ErrorMalformedType, err,
			"failed to unmarshal new-order request payload"))
		return
	}

	if err := nor.Validate(); err != nil {
		render.Error(w, r, err)
		return
	}

	// TODO(hs): gather all errors, so that we can build one response with ACME subproblems
	// include the nor.Validate() error here too, like in the example in the ACME RFC?

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

	var eak *acme.ExternalAccountKey
	if acmeProv.RequireEAB {
		if eak, err = db.GetExternalAccountKeyByAccountID(ctx, prov.GetID(), acc.ID); err != nil {
			render.Error(w, r, acme.WrapErrorISE(err, "error retrieving external account binding key"))
			return
		}
	}

	acmePolicy, err := newACMEPolicyEngine(eak)
	if err != nil {
		render.Error(w, r, acme.WrapErrorISE(err, "error creating ACME policy engine"))
		return
	}

	for _, identifier := range nor.Identifiers {
		// evaluate the ACME account level policy
		if err = isIdentifierAllowed(acmePolicy, identifier); err != nil {
			render.Error(w, r, acme.WrapError(acme.ErrorRejectedIdentifierType, err, "not authorized"))
			return
		}
		// evaluate the provisioner level policy
		orderIdentifier := provisioner.ACMEIdentifier{Type: provisioner.ACMEIdentifierType(identifier.Type), Value: identifier.Value}
		if err = prov.AuthorizeOrderIdentifier(ctx, orderIdentifier); err != nil {
			render.Error(w, r, acme.WrapError(acme.ErrorRejectedIdentifierType, err, "not authorized"))
			return
		}
		// evaluate the authority level policy
		if err = ca.AreSANsAllowed(ctx, []string{identifier.Value}); err != nil {
			render.Error(w, r, acme.WrapError(acme.ErrorRejectedIdentifierType, err, "not authorized"))
			return
		}
	}

	now := clock.Now()
	// New order.
	o := &acme.Order{
		AccountID:        acc.ID,
		ProvisionerID:    prov.GetID(),
		Status:           acme.StatusPending,
		Identifiers:      nor.Identifiers,
		ExpiresAt:        now.Add(defaultOrderExpiry),
		AuthorizationIDs: make([]string, len(nor.Identifiers)),
		NotBefore:        nor.NotBefore,
		NotAfter:         nor.NotAfter,
	}

	for i, identifier := range o.Identifiers {
		az := &acme.Authorization{
			AccountID:  acc.ID,
			Identifier: identifier,
			ExpiresAt:  o.ExpiresAt,
			Status:     acme.StatusPending,
		}
		if err := newAuthorization(ctx, az); err != nil {
			render.Error(w, r, err)
			return
		}
		o.AuthorizationIDs[i] = az.ID
	}

	if o.NotBefore.IsZero() {
		o.NotBefore = now
	}
	if o.NotAfter.IsZero() {
		o.NotAfter = o.NotBefore.Add(prov.DefaultTLSCertDuration())
	}

	// if request NotBefore was empty, then backdate the order.NotBefore (now)
	// to avoid timing issues.
	if nor.NotBefore.IsZero() {
		backdate := defaultOrderBackdate
		if bd := ca.GetBackdate(); bd != nil {
			backdate = *bd
		}
		o.NotBefore = o.NotBefore.Add(-backdate)
	}

	if err := db.CreateOrder(ctx, o); err != nil {
		render.Error(w, r, acme.WrapErrorISE(err, "error creating order"))
		return
	}

	linker.LinkOrder(ctx, o)

	w.Header().Set("Location", linker.GetLink(ctx, acme.OrderLinkType, o.ID))
	render.JSONStatus(w, r, o, http.StatusCreated)
}

func isIdentifierAllowed(acmePolicy policy.X509Policy, identifier acme.Identifier) error {
	if acmePolicy == nil {
		return nil
	}
	return acmePolicy.AreSANsAllowed([]string{identifier.Value})
}

func newACMEPolicyEngine(eak *acme.ExternalAccountKey) (policy.X509Policy, error) {
	if eak == nil {
		//nolint:nilnil,nolintlint // expected values
		return nil, nil
	}
	return policy.NewX509PolicyEngine(eak.Policy)
}

func trimIfWildcard(value string) (string, bool) {
	if strings.HasPrefix(value, "*.") {
		return strings.TrimPrefix(value, "*."), true
	}
	return value, false
}

func newAuthorization(ctx context.Context, az *acme.Authorization) error {
	value, isWildcard := trimIfWildcard(az.Identifier.Value)
	az.Wildcard = isWildcard
	az.Identifier = acme.Identifier{
		Value: value,
		Type:  az.Identifier.Type,
	}

	chTypes := challengeTypes(az)

	var err error
	az.Token, err = randutil.Alphanumeric(32)
	if err != nil {
		return acme.WrapErrorISE(err, "error generating random alphanumeric ID")
	}

	db := acme.MustDatabaseFromContext(ctx)
	prov := acme.MustProvisionerFromContext(ctx)
	az.Challenges = make([]*acme.Challenge, 0, len(chTypes))
	for _, typ := range chTypes {
		if !prov.IsChallengeEnabled(ctx, provisioner.ACMEChallenge(typ)) {
			continue
		}

		var target string
		switch az.Identifier.Type {
		case acme.WireUser:
			wireOptions, err := prov.GetOptions().GetWireOptions()
			if err != nil {
				return acme.WrapErrorISE(err, "failed getting Wire options")
			}
			target, err = wireOptions.GetOIDCOptions().EvaluateTarget("") // TODO(hs): determine if required by Wire
			if err != nil {
				return acme.WrapError(acme.ErrorMalformedType, err, "invalid Go template registered for 'target'")
			}
		case acme.WireDevice:
			wireID, err := wire.ParseDeviceID(az.Identifier.Value)
			if err != nil {
				return acme.WrapError(acme.ErrorMalformedType, err, "failed parsing WireDevice")
			}
			clientID, err := wire.ParseClientID(wireID.ClientID)
			if err != nil {
				return acme.WrapError(acme.ErrorMalformedType, err, "failed parsing ClientID")
			}
			wireOptions, err := prov.GetOptions().GetWireOptions()
			if err != nil {
				return acme.WrapErrorISE(err, "failed getting Wire options")
			}
			target, err = wireOptions.GetDPOPOptions().EvaluateTarget(clientID.DeviceID)
			if err != nil {
				return acme.WrapError(acme.ErrorMalformedType, err, "invalid Go template registered for 'target'")
			}
		}

		ch := &acme.Challenge{
			AccountID: az.AccountID,
			Value:     az.Identifier.Value,
			Type:      typ,
			Token:     az.Token,
			Status:    acme.StatusPending,
			Target:    target,
		}
		if err := db.CreateChallenge(ctx, ch); err != nil {
			return acme.WrapErrorISE(err, "error creating challenge")
		}
		az.Challenges = append(az.Challenges, ch)
	}
	if err = db.CreateAuthorization(ctx, az); err != nil {
		return acme.WrapErrorISE(err, "error creating authorization")
	}
	return nil
}

// GetOrder ACME api for retrieving an order.
func GetOrder(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
	}
	prov, err := provisionerFromContext(ctx)
	if err != nil {
		render.Error(w, r, err)
		return
	}

	o, err := db.GetOrder(ctx, chi.URLParam(r, "ordID"))
	if err != nil {
		render.Error(w, r, acme.WrapErrorISE(err, "error retrieving order"))
		return
	}
	if acc.ID != o.AccountID {
		render.Error(w, r, acme.NewError(acme.ErrorUnauthorizedType,
			"account '%s' does not own order '%s'", acc.ID, o.ID))
		return
	}
	if prov.GetID() != o.ProvisionerID {
		render.Error(w, r, acme.NewError(acme.ErrorUnauthorizedType,
			"provisioner '%s' does not own order '%s'", prov.GetID(), o.ID))
		return
	}
	if err = o.UpdateStatus(ctx, db); err != nil {
		render.Error(w, r, acme.WrapErrorISE(err, "error updating order status"))
		return
	}

	linker.LinkOrder(ctx, o)

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

// FinalizeOrder attempts to finalize an order and create a certificate.
func FinalizeOrder(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
	}
	prov, err := provisionerFromContext(ctx)
	if err != nil {
		render.Error(w, r, err)
		return
	}
	payload, err := payloadFromContext(ctx)
	if err != nil {
		render.Error(w, r, err)
		return
	}
	var fr FinalizeRequest
	if err := json.Unmarshal(payload.value, &fr); err != nil {
		render.Error(w, r, acme.WrapError(acme.ErrorMalformedType, err,
			"failed to unmarshal finalize-order request payload"))
		return
	}
	if err := fr.Validate(); err != nil {
		render.Error(w, r, err)
		return
	}

	o, err := db.GetOrder(ctx, chi.URLParam(r, "ordID"))
	if err != nil {
		render.Error(w, r, acme.WrapErrorISE(err, "error retrieving order"))
		return
	}
	if acc.ID != o.AccountID {
		render.Error(w, r, acme.NewError(acme.ErrorUnauthorizedType,
			"account '%s' does not own order '%s'", acc.ID, o.ID))
		return
	}
	if prov.GetID() != o.ProvisionerID {
		render.Error(w, r, acme.NewError(acme.ErrorUnauthorizedType,
			"provisioner '%s' does not own order '%s'", prov.GetID(), o.ID))
		return
	}

	ca := mustAuthority(ctx)
	if err = o.Finalize(ctx, db, fr.csr, ca, prov); err != nil {
		render.Error(w, r, acme.WrapErrorISE(err, "error finalizing order"))
		return
	}

	linker.LinkOrder(ctx, o)

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

// challengeTypes determines the types of challenges that should be used
// for the ACME authorization request.
func challengeTypes(az *acme.Authorization) []acme.ChallengeType {
	var chTypes []acme.ChallengeType

	switch az.Identifier.Type {
	case acme.IP:
		chTypes = []acme.ChallengeType{acme.HTTP01, acme.TLSALPN01}
	case acme.DNS:
		chTypes = []acme.ChallengeType{acme.DNS01}
		// HTTP and TLS challenges can only be used for identifiers without wildcards.
		if !az.Wildcard {
			chTypes = append(chTypes, []acme.ChallengeType{acme.HTTP01, acme.TLSALPN01}...)
		}
	case acme.PermanentIdentifier:
		chTypes = []acme.ChallengeType{acme.DEVICEATTEST01}
	case acme.WireUser:
		chTypes = []acme.ChallengeType{acme.WIREOIDC01}
	case acme.WireDevice:
		chTypes = []acme.ChallengeType{acme.WIREDPOP01}
	default:
		chTypes = []acme.ChallengeType{}
	}

	return chTypes
}
