package nosql

import (
	"context"
	"encoding/json"
	"time"

	"github.com/pkg/errors"
	"github.com/smallstep/certificates/authority/config"
	"github.com/smallstep/certificates/authority/mgmt"
	"github.com/smallstep/certificates/authority/status"
	"github.com/smallstep/nosql"
)

type dbAuthConfig struct {
	ID        string         `json:"id"`
	ASN1DN    *config.ASN1DN `json:"asn1dn"`
	Claims    *mgmt.Claims   `json:"claims"`
	Backdate  string         `json:"backdate,omitempty"`
	CreatedAt time.Time      `json:"createdAt"`
	DeletedAt time.Time      `json:"deletedAt"`
}

func (dbp *dbAuthConfig) clone() *dbAuthConfig {
	u := *dbp
	return &u
}

func (db *DB) getDBAuthConfigBytes(ctx context.Context, id string) ([]byte, error) {
	data, err := db.db.Get(authorityConfigsTable, []byte(id))
	if nosql.IsErrNotFound(err) {
		return nil, mgmt.NewError(mgmt.ErrorNotFoundType, "authConfig %s not found", id)
	} else if err != nil {
		return nil, errors.Wrapf(err, "error loading authConfig %s", id)
	}
	return data, nil
}

func (db *DB) getDBAuthConfig(ctx context.Context, id string) (*dbAuthConfig, error) {
	data, err := db.getDBAuthConfigBytes(ctx, id)
	if err != nil {
		return nil, err
	}

	var dba = new(dbAuthConfig)
	if err = json.Unmarshal(data, dba); err != nil {
		return nil, errors.Wrapf(err, "error unmarshaling authority %s into dbAuthConfig", id)
	}

	return dba, nil
}

// GetAuthConfig retrieves an AuthConfig configuration from the DB.
func (db *DB) GetAuthConfig(ctx context.Context, id string) (*mgmt.AuthConfig, error) {
	dba, err := db.getDBAuthConfig(ctx, id)
	if err != nil {
		return nil, err
	}

	provs, err := db.GetProvisioners(ctx)
	if err != nil {
		return nil, err
	}
	admins, err := db.GetAdmins(ctx)
	if err != nil {
		return nil, err
	}

	return &mgmt.AuthConfig{
		ID:           dba.ID,
		Admins:       admins,
		Provisioners: provs,
		ASN1DN:       dba.ASN1DN,
		Backdate:     dba.Backdate,
		Claims:       dba.Claims,
	}, nil
}

// CreateAuthConfig stores a new provisioner to the database.
func (db *DB) CreateAuthConfig(ctx context.Context, ac *mgmt.AuthConfig) error {
	var err error
	if ac.ID == "" {
		ac.ID, err = randID()
		if err != nil {
			return errors.Wrap(err, "error generating random id for provisioner")
		}
	}

	dba := &dbAuthConfig{
		ID:        ac.ID,
		ASN1DN:    ac.ASN1DN,
		Claims:    ac.Claims,
		Backdate:  ac.Backdate,
		CreatedAt: clock.Now(),
	}

	return db.save(ctx, dba.ID, dba, nil, "authConfig", authorityConfigsTable)
}

// UpdateAuthConfig saves an updated provisioner to the database.
func (db *DB) UpdateAuthConfig(ctx context.Context, ac *mgmt.AuthConfig) error {
	old, err := db.getDBAuthConfig(ctx, ac.ID)
	if err != nil {
		return err
	}

	nu := old.clone()

	// If the authority was active but is now deleted ...
	if old.DeletedAt.IsZero() && ac.Status == status.Deleted {
		nu.DeletedAt = clock.Now()
	}
	nu.Claims = ac.Claims
	nu.Backdate = ac.Backdate

	return db.save(ctx, old.ID, nu, old, "authConfig", authorityProvisionersTable)
}