package kv

import (
	"context"
	"fmt"

	"github.com/influxdata/influxdb/v2"
)

// DocumentIndex implements influxdb.DocumentIndex. It is used to access labels/owners of documents.
type DocumentIndex struct {
	service   *Service
	namespace string

	ctx      context.Context
	tx       Tx
	writable bool
}

// FindLabelByID retrieves a label by id.
func (i *DocumentIndex) FindLabelByID(id influxdb.ID) error {
	_, err := i.service.findLabelByID(i.ctx, i.tx, id)
	return err
}

// UsersOrgs retrieves a list of all orgs that a user is an accessor of.
func (i *DocumentIndex) UsersOrgs(userID influxdb.ID) ([]influxdb.ID, error) {
	f := influxdb.UserResourceMappingFilter{
		UserID:       userID,
		ResourceType: influxdb.OrgsResourceType,
	}

	ms, err := i.service.findUserResourceMappings(i.ctx, i.tx, f)
	if err != nil {
		return nil, err
	}

	ids := make([]influxdb.ID, 0, len(ms))
	for _, m := range ms {
		ids = append(ids, m.ResourceID)
	}

	return ids, nil
}

// IsOrgAccessor checks to see if the user is an accessor of the org provided. If the operation
// is writable it ensures that the user is owner.
func (i *DocumentIndex) IsOrgAccessor(userID influxdb.ID, orgID influxdb.ID) error {
	f := influxdb.UserResourceMappingFilter{
		UserID:       userID,
		ResourceType: influxdb.OrgsResourceType,
		ResourceID:   orgID,
	}

	if i.writable {
		f.UserType = influxdb.Owner
	}

	ms, err := i.service.findUserResourceMappings(i.ctx, i.tx, f)
	if err != nil {
		return err
	}

	for _, m := range ms {
		switch m.UserType {
		case influxdb.Owner, influxdb.Member:
			return nil
		default:
			continue
		}
	}

	return &influxdb.Error{
		Code: influxdb.EUnauthorized,
		Msg:  "user is not org member",
	}
}

func (i *DocumentIndex) ownerExists(ownerType string, ownerID influxdb.ID) error {
	switch ownerType {
	case "org":
		if _, err := i.service.findOrganizationByID(i.ctx, i.tx, ownerID); err != nil {
			return err
		}
	case "user":
		if _, err := i.service.findUserByID(i.ctx, i.tx, ownerID); err != nil {
			return err
		}
	default:
		return &influxdb.Error{
			Code: influxdb.EInternal,
			Msg:  fmt.Sprintf("unknown owner type %q", ownerType),
		}
	}

	return nil
}

// FindOrganizationByName retrieves the organization ID of the org provided.
func (i *DocumentIndex) FindOrganizationByName(org string) (influxdb.ID, error) {
	o, err := i.service.findOrganizationByName(i.ctx, i.tx, org)
	if err != nil {
		return influxdb.InvalidID(), err
	}
	return o.ID, nil
}

// FindOrganizationByID checks if the org existence by the org id provided.
func (i *DocumentIndex) FindOrganizationByID(id influxdb.ID) error {
	_, err := i.service.findOrganizationByID(i.ctx, i.tx, id)
	if err != nil {
		return err
	}
	return nil
}

// GetDocumentsAccessors retrieves the list of accessors of a document.
func (i *DocumentIndex) GetDocumentsAccessors(docID influxdb.ID) ([]influxdb.ID, error) {
	f := influxdb.UserResourceMappingFilter{
		ResourceType: influxdb.DocumentsResourceType,
		ResourceID:   docID,
	}
	if i.writable {
		f.UserType = influxdb.Owner
	}
	ms, err := i.service.findUserResourceMappings(i.ctx, i.tx, f)
	if err != nil {
		return nil, err
	}

	ids := make([]influxdb.ID, 0, len(ms))
	for _, m := range ms {
		if m.MappingType == influxdb.UserMappingType {
			continue
		}
		// TODO(desa): this is really an orgID, eventually we should support users and org as owners of documents
		ids = append(ids, m.UserID)
	}

	return ids, nil
}

// GetAccessorsDocuments retrieves the list of documents a user is allowed to access.
func (i *DocumentIndex) GetAccessorsDocuments(ownerType string, ownerID influxdb.ID) ([]influxdb.ID, error) {
	if err := i.ownerExists(ownerType, ownerID); err != nil {
		return nil, err
	}

	f := influxdb.UserResourceMappingFilter{
		UserID:       ownerID,
		ResourceType: influxdb.DocumentsResourceType,
	}
	if i.writable {
		f.UserType = influxdb.Owner
	}
	ms, err := i.service.findUserResourceMappings(i.ctx, i.tx, f)
	if err != nil {
		return nil, err
	}

	ids := make([]influxdb.ID, 0, len(ms))
	for _, m := range ms {
		ids = append(ids, m.ResourceID)
	}

	return ids, nil
}

// DocumentDecorator is used to communication the decoration of documents to the
// document store.
type DocumentDecorator struct {
	data   bool
	labels bool
	orgs   bool

	writable bool
}

// IncludeContent signals that the document should include its content when returned.
func (d *DocumentDecorator) IncludeContent() error {
	if d.writable {
		return &influxdb.Error{
			Code: influxdb.EInternal,
			Msg:  "cannot include data in document",
		}
	}

	d.data = true
	return nil
}

// IncludeLabels signals that the document should include its labels when returned.
func (d *DocumentDecorator) IncludeLabels() error {
	if d.writable {
		return &influxdb.Error{
			Code: influxdb.EInternal,
			Msg:  "cannot include labels in document",
		}
	}

	d.labels = true
	return nil
}

// IncludeOwner signals that the document should include its owner.
func (d *DocumentDecorator) IncludeOrganizations() error {
	if d.writable {
		return &influxdb.Error{
			Code: influxdb.EInternal,
			Msg:  "cannot include labels in document",
		}
	}

	d.orgs = true
	return nil
}
