package admin

import (
	"context"
	"encoding/json"
	"fmt"

	"github.com/pkg/errors"
	"go.step.sm/linkedca"
)

const (
	// DefaultAuthorityID is the default AuthorityID. This will be the ID
	// of the first Authority created, as well as the default AuthorityID
	// if one is not specified in the configuration.
	DefaultAuthorityID = "00000000-0000-0000-0000-000000000000"
)

// ErrNotFound is an error that should be used by the authority.DB interface to
// indicate that an entity does not exist.
var ErrNotFound = errors.New("not found")

// UnmarshalProvisionerDetails unmarshals details type to the specific provisioner details.
func UnmarshalProvisionerDetails(typ linkedca.Provisioner_Type, data []byte) (*linkedca.ProvisionerDetails, error) {
	var v linkedca.ProvisionerDetails
	switch typ {
	case linkedca.Provisioner_JWK:
		v.Data = new(linkedca.ProvisionerDetails_JWK)
	case linkedca.Provisioner_OIDC:
		v.Data = new(linkedca.ProvisionerDetails_OIDC)
	case linkedca.Provisioner_GCP:
		v.Data = new(linkedca.ProvisionerDetails_GCP)
	case linkedca.Provisioner_AWS:
		v.Data = new(linkedca.ProvisionerDetails_AWS)
	case linkedca.Provisioner_AZURE:
		v.Data = new(linkedca.ProvisionerDetails_Azure)
	case linkedca.Provisioner_ACME:
		v.Data = new(linkedca.ProvisionerDetails_ACME)
	case linkedca.Provisioner_X5C:
		v.Data = new(linkedca.ProvisionerDetails_X5C)
	case linkedca.Provisioner_K8SSA:
		v.Data = new(linkedca.ProvisionerDetails_K8SSA)
	case linkedca.Provisioner_SSHPOP:
		v.Data = new(linkedca.ProvisionerDetails_SSHPOP)
	case linkedca.Provisioner_SCEP:
		v.Data = new(linkedca.ProvisionerDetails_SCEP)
	case linkedca.Provisioner_NEBULA:
		v.Data = new(linkedca.ProvisionerDetails_Nebula)
	default:
		return nil, fmt.Errorf("unsupported provisioner type %s", typ)
	}

	if err := json.Unmarshal(data, v.Data); err != nil {
		return nil, err
	}
	return &linkedca.ProvisionerDetails{Data: v.Data}, nil
}

// DB is the DB interface expected by the step-ca Admin API.
type DB interface {
	CreateProvisioner(ctx context.Context, prov *linkedca.Provisioner) error
	GetProvisioner(ctx context.Context, id string) (*linkedca.Provisioner, error)
	GetProvisioners(ctx context.Context) ([]*linkedca.Provisioner, error)
	UpdateProvisioner(ctx context.Context, prov *linkedca.Provisioner) error
	DeleteProvisioner(ctx context.Context, id string) error

	CreateAdmin(ctx context.Context, admin *linkedca.Admin) error
	GetAdmin(ctx context.Context, id string) (*linkedca.Admin, error)
	GetAdmins(ctx context.Context) ([]*linkedca.Admin, error)
	UpdateAdmin(ctx context.Context, admin *linkedca.Admin) error
	DeleteAdmin(ctx context.Context, id string) error

	CreateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error
	GetAuthorityPolicy(ctx context.Context) (*linkedca.Policy, error)
	UpdateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error
	DeleteAuthorityPolicy(ctx context.Context) error
}

type dbKey struct{}

// NewContext adds the given admin database to the context.
func NewContext(ctx context.Context, db DB) context.Context {
	return context.WithValue(ctx, dbKey{}, db)
}

// FromContext returns the current admin database from the given context.
func FromContext(ctx context.Context) (db DB, ok bool) {
	db, ok = ctx.Value(dbKey{}).(DB)
	return
}

// MustFromContext returns the current admin database from the given context. It
// will panic if it's not in the context.
func MustFromContext(ctx context.Context) DB {
	if db, ok := FromContext(ctx); !ok {
		panic("admin database is not in the context")
	} else {
		return db
	}
}

// MockDB is an implementation of the DB interface that should only be used as
// a mock in tests.
type MockDB struct {
	MockCreateProvisioner func(ctx context.Context, prov *linkedca.Provisioner) error
	MockGetProvisioner    func(ctx context.Context, id string) (*linkedca.Provisioner, error)
	MockGetProvisioners   func(ctx context.Context) ([]*linkedca.Provisioner, error)
	MockUpdateProvisioner func(ctx context.Context, prov *linkedca.Provisioner) error
	MockDeleteProvisioner func(ctx context.Context, id string) error

	MockCreateAdmin func(ctx context.Context, adm *linkedca.Admin) error
	MockGetAdmin    func(ctx context.Context, id string) (*linkedca.Admin, error)
	MockGetAdmins   func(ctx context.Context) ([]*linkedca.Admin, error)
	MockUpdateAdmin func(ctx context.Context, adm *linkedca.Admin) error
	MockDeleteAdmin func(ctx context.Context, id string) error

	MockCreateAuthorityPolicy func(ctx context.Context, policy *linkedca.Policy) error
	MockGetAuthorityPolicy    func(ctx context.Context) (*linkedca.Policy, error)
	MockUpdateAuthorityPolicy func(ctx context.Context, policy *linkedca.Policy) error
	MockDeleteAuthorityPolicy func(ctx context.Context) error

	MockError error
	MockRet1  interface{}
}

// CreateProvisioner mock.
func (m *MockDB) CreateProvisioner(ctx context.Context, prov *linkedca.Provisioner) error {
	if m.MockCreateProvisioner != nil {
		return m.MockCreateProvisioner(ctx, prov)
	} else if m.MockError != nil {
		return m.MockError
	}
	return m.MockError
}

// GetProvisioner mock.
func (m *MockDB) GetProvisioner(ctx context.Context, id string) (*linkedca.Provisioner, error) {
	if m.MockGetProvisioner != nil {
		return m.MockGetProvisioner(ctx, id)
	} else if m.MockError != nil {
		return nil, m.MockError
	}
	return m.MockRet1.(*linkedca.Provisioner), m.MockError
}

// GetProvisioners mock
func (m *MockDB) GetProvisioners(ctx context.Context) ([]*linkedca.Provisioner, error) {
	if m.MockGetProvisioners != nil {
		return m.MockGetProvisioners(ctx)
	} else if m.MockError != nil {
		return nil, m.MockError
	}
	return m.MockRet1.([]*linkedca.Provisioner), m.MockError
}

// UpdateProvisioner mock
func (m *MockDB) UpdateProvisioner(ctx context.Context, prov *linkedca.Provisioner) error {
	if m.MockUpdateProvisioner != nil {
		return m.MockUpdateProvisioner(ctx, prov)
	}
	return m.MockError
}

// DeleteProvisioner mock
func (m *MockDB) DeleteProvisioner(ctx context.Context, id string) error {
	if m.MockDeleteProvisioner != nil {
		return m.MockDeleteProvisioner(ctx, id)
	}
	return m.MockError
}

// CreateAdmin mock
func (m *MockDB) CreateAdmin(ctx context.Context, admin *linkedca.Admin) error {
	if m.MockCreateAdmin != nil {
		return m.MockCreateAdmin(ctx, admin)
	}
	return m.MockError
}

// GetAdmin mock.
func (m *MockDB) GetAdmin(ctx context.Context, id string) (*linkedca.Admin, error) {
	if m.MockGetAdmin != nil {
		return m.MockGetAdmin(ctx, id)
	} else if m.MockError != nil {
		return nil, m.MockError
	}
	return m.MockRet1.(*linkedca.Admin), m.MockError
}

// GetAdmins mock
func (m *MockDB) GetAdmins(ctx context.Context) ([]*linkedca.Admin, error) {
	if m.MockGetAdmins != nil {
		return m.MockGetAdmins(ctx)
	} else if m.MockError != nil {
		return nil, m.MockError
	}
	return m.MockRet1.([]*linkedca.Admin), m.MockError
}

// UpdateAdmin mock
func (m *MockDB) UpdateAdmin(ctx context.Context, adm *linkedca.Admin) error {
	if m.MockUpdateAdmin != nil {
		return m.MockUpdateAdmin(ctx, adm)
	}
	return m.MockError
}

// DeleteAdmin mock
func (m *MockDB) DeleteAdmin(ctx context.Context, id string) error {
	if m.MockDeleteAdmin != nil {
		return m.MockDeleteAdmin(ctx, id)
	}
	return m.MockError
}

// CreateAuthorityPolicy mock
func (m *MockDB) CreateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error {
	if m.MockCreateAuthorityPolicy != nil {
		return m.MockCreateAuthorityPolicy(ctx, policy)
	}
	return m.MockError
}

// GetAuthorityPolicy mock
func (m *MockDB) GetAuthorityPolicy(ctx context.Context) (*linkedca.Policy, error) {
	if m.MockGetAuthorityPolicy != nil {
		return m.MockGetAuthorityPolicy(ctx)
	}
	return m.MockRet1.(*linkedca.Policy), m.MockError
}

// UpdateAuthorityPolicy mock
func (m *MockDB) UpdateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error {
	if m.MockUpdateAuthorityPolicy != nil {
		return m.MockUpdateAuthorityPolicy(ctx, policy)
	}
	return m.MockError
}

// DeleteAuthorityPolicy mock
func (m *MockDB) DeleteAuthorityPolicy(ctx context.Context) error {
	if m.MockDeleteAuthorityPolicy != nil {
		return m.MockDeleteAuthorityPolicy(ctx)
	}
	return m.MockError
}
