// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package certutil

import (
	"crypto/x509"
	"encoding/pem"
	"fmt"
)

// Source of the issuance request: sign implies that the key material was
// generated by the user and submitted via a CSR request but only ACL level
// validation was applied; issue implies that Vault created the key material
// on behalf of the user with ACL level validation occurring; ACME implies
// that the user submitted a CSR and that additional ACME validation has
// occurred before sending the request to the external service for
// construction.
type CIEPSIssuanceMode string

const (
	SignCIEPSMode  = "sign"
	IssueCIEPSMode = "issue"
	ACMECIEPSMode  = "acme"
	ICACIEPSMode   = "ica"
)

// Configuration of the issuer and mount at the time of this request;
// states the issuer's templated AIA information (falling back to the
// mount-global config if no per-issuer AIA info is set, the issuer's
// leaf_not_after_behavior (permit/truncate/err) for TTLs exceeding the
// issuer's validity period, and the mount's default and max TTL.
type CIEPSIssuanceConfig struct {
	AIAValues            *URLEntries `json:"aia_values"`
	LeafNotAfterBehavior string      `json:"leaf_not_after_behavior"`
	MountDefaultTTL      string      `json:"mount_default_ttl"`
	MountMaxTTL          string      `json:"mount_max_ttl"`
}

// Structured parameters sent by Vault or explicitly validated by Vault
// prior to sending.
type CIEPSVaultParams struct {
	PolicyName string `json:"policy_name,omitempty"`
	Mount      string `json:"mount"`
	Namespace  string `json:"ns"`

	// These indicate the type of the cluster node talking to the CIEPS
	// service. When IsPerfStandby=true, setting StoreCert=true in the
	// response will result in Vault forwarding the client's request
	// up to the Performance Secondary's active node and re-trying the
	// operation (including re-submitting the request to the CIEPS
	// service).
	//
	// Any response returned by the CIEPS service in this case will be
	// ignored and not signed by the CA's keys.
	//
	// IsPRSecondary is set to false when a local mount is used on a
	// PR Secondary; in this scenario, PR Secondary nodes behave like
	// PR Primary nodes. From a CIEPS service perspective, no behavior
	// difference is expected between PR Primary and PR Secondary nodes;
	// both will issue and store certificates on their active nodes.
	// This information is included for audit tracking purposes.
	IsPerfStandby bool `json:"vault_is_performance_standby"`
	IsPRSecondary bool `json:"vault_is_performance_secondary"`

	IssuanceMode CIEPSIssuanceMode `json:"issuance_mode"`

	GeneratedKey bool `json:"vault_generated_private_key"`

	IssuerName string `json:"requested_issuer_name"`
	IssuerID   string `json:"requested_issuer_id"`
	IssuerCert string `json:"requested_issuer_cert"`

	Config CIEPSIssuanceConfig `json:"requested_issuance_config"`
}

// Outer request object sent by Vault to the external CIEPS service.
//
// The top-level fields denote properties about the CIEPS request,
// with various request fields containing untrusted and trusted input
// respectively.
type CIEPSRequest struct {
	Version int    `json:"request_version"`
	UUID    string `json:"request_uuid"`
	Sync    bool   `json:"synchronous"`

	UserRequestKV     map[string]interface{} `json:"user_request_key_values"`
	IdentityRequestKV map[string]interface{} `json:"identity_request_key_values,omitempty"`
	ACMERequestKV     map[string]interface{} `json:"acme_request_key_values,omitempty"`
	VaultRequestKV    CIEPSVaultParams       `json:"vault_request_values"`

	// Vault guarantees that UserRequestKV will contain a csr parameter
	// for all request types; this field is useful for engine implementations
	// to have in parsed format. We assume that this is sent in PEM format,
	// aligning with other Vault requests.
	ParsedCSR *x509.CertificateRequest `json:"-"`
}

func (req *CIEPSRequest) ParseUserCSR() error {
	csrValueRaw, present := req.UserRequestKV["csr"]
	if !present {
		return fmt.Errorf("missing expected 'csr' attribute on the request")
	}

	csrValue, ok := csrValueRaw.(string)
	if !ok {
		return fmt.Errorf("unexpected type of 'csr' attribute: %T", csrValueRaw)
	}

	if csrValue == "" {
		return fmt.Errorf("unexpectedly empty 'csr' attribute on the request")
	}

	block, rest := pem.Decode([]byte(csrValue))
	if len(rest) > 0 {
		return fmt.Errorf("failed to decode 'csr': %v bytes of trailing data after PEM block", len(rest))
	}
	if block == nil {
		return fmt.Errorf("failed to decode 'csr' PEM block")
	}

	csr, err := x509.ParseCertificateRequest(block.Bytes)
	if err != nil {
		return fmt.Errorf("failed to parse certificate request: %w", err)
	}

	req.ParsedCSR = csr
	return nil
}

// Expected response object from the external CIEPS service.
//
// When parsing, Vault will disallow unknown fields, failing the
// parse if unknown fields are sent.
type CIEPSResponse struct {
	UUID              string            `json:"request_uuid"`
	Error             string            `json:"error"`
	Warnings          []string          `json:"warnings"`
	Certificate       string            `json:"certificate"`
	ParsedCertificate *x509.Certificate `json:"-"`
	IssuerRef         string            `json:"issuer_ref,omitempty"`
	StoreCert         bool              `json:"store_certificate"`
	GenerateLease     bool              `json:"generate_lease"`
}

func (c *CIEPSResponse) MarshalCertificate() error {
	if c.ParsedCertificate == nil || len(c.ParsedCertificate.Raw) == 0 {
		return fmt.Errorf("no certificate present")
	}

	pem := pem.EncodeToMemory(&pem.Block{
		Type:  "CERTIFICATE",
		Bytes: c.ParsedCertificate.Raw,
	})
	if len(pem) == 0 {
		return fmt.Errorf("failed to generate PEM: no body")
	}
	c.Certificate = string(pem)

	return nil
}
