package tenant

import (
	"context"

	"github.com/influxdata/influxdb/v2"
	icontext "github.com/influxdata/influxdb/v2/context"
	"github.com/influxdata/influxdb/v2/kv"
)

// Returns a single organization by ID.
func (s *Service) FindOrganizationByID(ctx context.Context, id influxdb.ID) (*influxdb.Organization, error) {
	var org *influxdb.Organization
	err := s.store.View(ctx, func(tx kv.Tx) error {
		o, err := s.store.GetOrg(ctx, tx, id)
		if err != nil {
			return err
		}
		org = o
		return nil
	})

	if err != nil {
		return nil, err
	}

	return org, nil
}

// Returns the first organization that matches filter.
func (s *Service) FindOrganization(ctx context.Context, filter influxdb.OrganizationFilter) (*influxdb.Organization, error) {
	if filter.ID != nil {
		return s.FindOrganizationByID(ctx, *filter.ID)
	}

	if filter.Name == nil {
		return nil, influxdb.ErrInvalidOrgFilter
	}

	var org *influxdb.Organization
	err := s.store.View(ctx, func(tx kv.Tx) error {
		o, err := s.store.GetOrgByName(ctx, tx, *filter.Name)
		if err != nil {
			return err
		}
		org = o
		return nil
	})

	if err != nil {
		return nil, err
	}

	return org, nil
}

// Returns a list of organizations that match filter and the total count of matching organizations.
// Additional options provide pagination & sorting.
func (s *Service) FindOrganizations(ctx context.Context, filter influxdb.OrganizationFilter, opt ...influxdb.FindOptions) ([]*influxdb.Organization, int, error) {
	// if im given a id or a name I know I can only return 1
	if filter.ID != nil || filter.Name != nil {
		org, err := s.FindOrganization(ctx, filter)
		if err != nil {
			return nil, 0, err
		}
		return []*influxdb.Organization{org}, 1, nil
	}

	var orgs []*influxdb.Organization

	if filter.UserID != nil {
		// find urms for orgs with this user
		urms, _, err := s.FindUserResourceMappings(ctx, influxdb.UserResourceMappingFilter{
			UserID:       *filter.UserID,
			ResourceType: influxdb.OrgsResourceType,
		}, opt...)
		if err != nil {
			return nil, 0, err
		}
		// find orgs by the urm's resource ids.
		for _, urm := range urms {
			o, err := s.FindOrganizationByID(ctx, urm.ResourceID)
			if err == nil {
				// if there is an error then this is a crufty urm and we should just move on
				orgs = append(orgs, o)
			}
		}

		return orgs, len(orgs), nil
	}

	err := s.store.View(ctx, func(tx kv.Tx) error {
		os, err := s.store.ListOrgs(ctx, tx, opt...)
		if err != nil {
			return err
		}
		orgs = os
		return nil
	})

	if err != nil {
		return nil, 0, err
	}

	return orgs, len(orgs), err
}

// Creates a new organization and sets b.ID with the new identifier.
func (s *Service) CreateOrganization(ctx context.Context, o *influxdb.Organization) error {
	err := s.store.Update(ctx, func(tx kv.Tx) error {
		err := s.store.CreateOrg(ctx, tx, o)
		if err != nil {
			return err
		}

		tb := &influxdb.Bucket{
			OrgID:           o.ID,
			Type:            influxdb.BucketTypeSystem,
			Name:            influxdb.TasksSystemBucketName,
			RetentionPeriod: influxdb.TasksSystemBucketRetention,
			Description:     "System bucket for task logs",
		}

		if err := s.store.CreateBucket(ctx, tx, tb); err != nil {
			return err
		}

		mb := &influxdb.Bucket{
			OrgID:           o.ID,
			Type:            influxdb.BucketTypeSystem,
			Name:            influxdb.MonitoringSystemBucketName,
			RetentionPeriod: influxdb.MonitoringSystemBucketRetention,
			Description:     "System bucket for monitoring logs",
		}

		if err := s.store.CreateBucket(ctx, tx, mb); err != nil {
			return err
		}

		// create assiciated URM
		userID, err := icontext.GetUserID(ctx)
		if err == nil {
			// if I am given a userid i can associate the user as the org owner
			err = s.store.CreateURM(ctx, tx, &influxdb.UserResourceMapping{
				UserID:       userID,
				UserType:     influxdb.Owner,
				MappingType:  influxdb.UserMappingType,
				ResourceType: influxdb.OrgsResourceType,
				ResourceID:   o.ID,
			})
			if err != nil {
				return err
			}
		}
		return nil
	})
	return err
}

// Updates a single organization with changeset.
// Returns the new organization state after update.
func (s *Service) UpdateOrganization(ctx context.Context, id influxdb.ID, upd influxdb.OrganizationUpdate) (*influxdb.Organization, error) {
	var org *influxdb.Organization
	err := s.store.Update(ctx, func(tx kv.Tx) error {
		o, err := s.store.UpdateOrg(ctx, tx, id, upd)
		if err != nil {
			return err
		}
		org = o
		return nil
	})
	if err != nil {
		return nil, err
	}
	return org, nil
}

// DeleteOrganization removes a organization by ID and its dependent resources.
func (s *Service) DeleteOrganization(ctx context.Context, id influxdb.ID) error {
	err := s.store.Update(ctx, func(tx kv.Tx) error {
		// clean up the buckets for this organization
		filter := BucketFilter{
			OrganizationID: &id,
		}
		bs, err := s.store.ListBuckets(ctx, tx, filter)
		if err != nil {
			return err
		}
		for _, b := range bs {
			if err := s.store.DeleteBucket(ctx, tx, b.ID); err != nil {
				if err != ErrBucketNotFound {
					return err
				}
			}
			if err := s.removeResourceRelations(ctx, tx, b.ID); err != nil {
				return err
			}
		}

		if err := s.removeResourceRelations(ctx, tx, id); err != nil {
			return err
		}

		return s.store.DeleteOrg(ctx, tx, id)
	})
	return err
}
