package ssh

import (
	"context"
	"crypto/rand"
	"errors"
	"fmt"

	"github.com/hashicorp/vault/sdk/framework"
	"github.com/hashicorp/vault/sdk/logical"
)

type keySpecs struct {
	Type string
	Bits int
}

func pathIssue(b *backend) *framework.Path {
	return &framework.Path{
		Pattern: "issue/" + framework.GenericNameWithAtRegex("role"),

		Operations: map[logical.Operation]framework.OperationHandler{
			logical.UpdateOperation: &framework.PathOperation{
				Callback: b.pathIssue,
			},
		},
		Fields: map[string]*framework.FieldSchema{
			"role": {
				Type:        framework.TypeString,
				Description: `The desired role with configuration for this request.`,
			},
			"key_type": {
				Type:        framework.TypeString,
				Description: "Specifies the desired key type; must be `rsa`, `ed25519` or `ec`",
				Default:     "rsa",
			},
			"key_bits": {
				Type:        framework.TypeInt,
				Description: "Specifies the number of bits to use for the generated keys.",
				Default:     0,
			},
			"ttl": {
				Type: framework.TypeDurationSecond,
				Description: `The requested Time To Live for the SSH certificate;
sets the expiration date. If not specified
the role default, backend default, or system
default TTL is used, in that order. Cannot
be later than the role max TTL.`,
			},
			"valid_principals": {
				Type:        framework.TypeString,
				Description: `Valid principals, either usernames or hostnames, that the certificate should be signed for.`,
			},
			"cert_type": {
				Type:        framework.TypeString,
				Description: `Type of certificate to be created; either "user" or "host".`,
				Default:     "user",
			},
			"key_id": {
				Type:        framework.TypeString,
				Description: `Key id that the created certificate should have. If not specified, the display name of the token will be used.`,
			},
			"critical_options": {
				Type:        framework.TypeMap,
				Description: `Critical options that the certificate should be signed for.`,
			},
			"extensions": {
				Type:        framework.TypeMap,
				Description: `Extensions that the certificate should be signed for.`,
			},
		},
		HelpSynopsis:    pathIssueHelpSyn,
		HelpDescription: pathIssueHelpDesc,
	}
}

func (b *backend) pathIssue(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
	// Get the role
	roleName := data.Get("role").(string)
	role, err := b.getRole(ctx, req.Storage, roleName)
	if err != nil {
		return nil, err
	}
	if role == nil {
		return logical.ErrorResponse(fmt.Sprintf("unknown role: %s", roleName)), nil
	}

	if role.KeyType != "ca" {
		return logical.ErrorResponse("role key type '%s' not allowed to issue key pairs", role.KeyType), nil
	}

	// Validate and extract key specifications
	keySpecs, err := extractKeySpecs(role, data)
	if err != nil {
		return logical.ErrorResponse(err.Error()), nil
	}

	// Issue certificate
	return b.pathIssueCertificate(ctx, req, data, role, keySpecs)
}

func (b *backend) pathIssueCertificate(ctx context.Context, req *logical.Request, data *framework.FieldData, role *sshRole, keySpecs *keySpecs) (*logical.Response, error) {
	publicKey, privateKey, err := generateSSHKeyPair(rand.Reader, keySpecs.Type, keySpecs.Bits)
	if err != nil {
		return nil, err
	}

	// Sign key
	userPublicKey, err := parsePublicSSHKey(publicKey)
	if err != nil {
		return logical.ErrorResponse(fmt.Sprintf("failed to parse public_key as SSH key: %s", err)), nil
	}

	response, err := b.pathSignIssueCertificateHelper(ctx, req, data, role, userPublicKey)
	if err != nil {
		return nil, err
	}
	if response.IsError() {
		return response, nil
	}

	// Additional to sign response
	response.Data["private_key"] = privateKey
	response.Data["private_key_type"] = keySpecs.Type

	return response, nil
}

func extractKeySpecs(role *sshRole, data *framework.FieldData) (*keySpecs, error) {
	keyType := data.Get("key_type").(string)
	keyBits := data.Get("key_bits").(int)
	keySpecs := keySpecs{
		Type: keyType,
		Bits: keyBits,
	}

	keyTypeToMapKey := createKeyTypeToMapKey(keyType, keyBits)

	if len(role.AllowedUserKeyTypesLengths) != 0 {
		var keyAllowed bool
		var bitsAllowed bool

	keyTypeAliasesLoop:
		for _, keyTypeAlias := range keyTypeToMapKey[keyType] {
			allowedValues, allowed := role.AllowedUserKeyTypesLengths[keyTypeAlias]
			if !allowed {
				continue
			}
			keyAllowed = true

			for _, value := range allowedValues {
				if value == keyBits {
					bitsAllowed = true
					break keyTypeAliasesLoop
				}
			}
		}

		if !keyAllowed {
			return nil, errors.New("provided key_type value not in allowed_user_key_types")
		}

		if !bitsAllowed {
			return nil, errors.New("provided key_bits value not in list of role's allowed_user_key_types")
		}
	}

	return &keySpecs, nil
}

const pathIssueHelpSyn = `
Request a certificate using a certain role with the provided details.
`

const pathIssueHelpDesc = `
This path allows requesting a certificate to be issued according to the
policy of the given role. The certificate will only be issued if the
requested details are allowed by the role policy.

This path returns a certificate and a private key. If you want a workflow
that does not expose a private key, generate a CSR locally and use the
sign path instead.
`
