package cassandra

import (
	"context"
	"fmt"
	"time"

	"github.com/fatih/structs"
	"github.com/gocql/gocql"
	"github.com/hashicorp/vault/logical"
	"github.com/hashicorp/vault/logical/framework"
)

const (
	defaultCreationCQL = `CREATE USER '{{username}}' WITH PASSWORD '{{password}}' NOSUPERUSER;`
	defaultRollbackCQL = `DROP USER '{{username}}';`
)

func pathRoles(b *backend) *framework.Path {
	return &framework.Path{
		Pattern: "roles/" + framework.GenericNameRegex("name"),
		Fields: map[string]*framework.FieldSchema{
			"name": &framework.FieldSchema{
				Type:        framework.TypeString,
				Description: "Name of the role",
			},

			"creation_cql": &framework.FieldSchema{
				Type:    framework.TypeString,
				Default: defaultCreationCQL,
				Description: `CQL to create a user and optionally grant
authorization. If not supplied, a default that
creates non-superuser accounts with the built-in
password authenticator will be used; no
authorization grants will be configured. Separate
statements by semicolons; use @file to load from a
file. Valid template values are '{{username}}' and
'{{password}}' -- the single quotes are important!`,
			},

			"rollback_cql": &framework.FieldSchema{
				Type:    framework.TypeString,
				Default: defaultRollbackCQL,
				Description: `CQL to roll back an account operation. This will
be used if there is an error during execution of a
statement passed in via the "creation_cql" parameter
parameter. The default simply drops the user, which
should generally be sufficient. Separate statements
by semicolons; use @file to load from a file. Valid
template values are '{{username}}' and
'{{password}}' -- the single quotes are important!`,
			},

			"lease": &framework.FieldSchema{
				Type:        framework.TypeString,
				Default:     "4h",
				Description: "The lease length; defaults to 4 hours",
			},

			"consistency": &framework.FieldSchema{
				Type:        framework.TypeString,
				Default:     "Quorum",
				Description: "The consistency level for the operations; defaults to Quorum.",
			},
		},

		Callbacks: map[logical.Operation]framework.OperationFunc{
			logical.ReadOperation:   b.pathRoleRead,
			logical.UpdateOperation: b.pathRoleCreate,
			logical.DeleteOperation: b.pathRoleDelete,
		},

		HelpSynopsis:    pathRoleHelpSyn,
		HelpDescription: pathRoleHelpDesc,
	}
}

func getRole(ctx context.Context, s logical.Storage, n string) (*roleEntry, error) {
	entry, err := s.Get(ctx, "role/"+n)
	if err != nil {
		return nil, err
	}
	if entry == nil {
		return nil, nil
	}

	var result roleEntry
	if err := entry.DecodeJSON(&result); err != nil {
		return nil, err
	}

	return &result, nil
}

func (b *backend) pathRoleDelete(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
	err := req.Storage.Delete(ctx, "role/"+data.Get("name").(string))
	if err != nil {
		return nil, err
	}

	return nil, nil
}

func (b *backend) pathRoleRead(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
	role, err := getRole(ctx, req.Storage, data.Get("name").(string))
	if err != nil {
		return nil, err
	}
	if role == nil {
		return nil, nil
	}

	return &logical.Response{
		Data: structs.New(role).Map(),
	}, nil
}

func (b *backend) pathRoleCreate(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
	name := data.Get("name").(string)

	creationCQL := data.Get("creation_cql").(string)

	rollbackCQL := data.Get("rollback_cql").(string)

	leaseRaw := data.Get("lease").(string)
	lease, err := time.ParseDuration(leaseRaw)
	if err != nil {
		return logical.ErrorResponse(fmt.Sprintf(
			"Error parsing lease value of %s: %s", leaseRaw, err)), nil
	}

	consistencyStr := data.Get("consistency").(string)
	_, err = gocql.ParseConsistencyWrapper(consistencyStr)
	if err != nil {
		return logical.ErrorResponse(fmt.Sprintf(
			"Error parsing consistency value of %q: %v", consistencyStr, err)), nil
	}

	entry := &roleEntry{
		Lease:       lease,
		CreationCQL: creationCQL,
		RollbackCQL: rollbackCQL,
		Consistency: consistencyStr,
	}

	// Store it
	entryJSON, err := logical.StorageEntryJSON("role/"+name, entry)
	if err != nil {
		return nil, err
	}
	if err := req.Storage.Put(ctx, entryJSON); err != nil {
		return nil, err
	}

	return nil, nil
}

type roleEntry struct {
	CreationCQL string        `json:"creation_cql" structs:"creation_cql"`
	Lease       time.Duration `json:"lease" structs:"lease"`
	RollbackCQL string        `json:"rollback_cql" structs:"rollback_cql"`
	Consistency string        `json:"consistency" structs:"consistency"`
}

const pathRoleHelpSyn = `
Manage the roles that can be created with this backend.
`

const pathRoleHelpDesc = `
This path lets you manage the roles that can be created with this backend.

The "creation_cql" parameter customizes the CQL string used to create users
and assign them grants. This can be a sequence of CQL queries separated by
semicolons. Some substitution will be done to the CQL string for certain keys.
The names of the variables must be surrounded by '{{' and '}}' to be replaced.
Note that it is important that single quotes are used, not double quotes.

  * "username" - The random username generated for the DB user.

  * "password" - The random password generated for the DB user.

If no "creation_cql" parameter is given, a default will be used:

` + defaultCreationCQL + `

This default should be suitable for Cassandra installations using the password
authenticator but not configured to use authorization.

Similarly, the "rollback_cql" is used if user creation fails, in the absence of
Cassandra transactions. The default should be suitable for almost any
instance of Cassandra:

` + defaultRollbackCQL + `

"lease" the lease time; if not set the mount/system defaults are used.
`
