// Copyright (C) The Arvados Authors. All rights reserved.
//
// SPDX-License-Identifier: Apache-2.0

package arvados

import (
	"log"
	"os"
	"strings"
	"time"
)

func (fs *customFileSystem) defaultUUID(uuid string) (string, error) {
	if uuid != "" {
		return uuid, nil
	}
	var resp User
	err := fs.RequestAndDecode(&resp, "GET", "arvados/v1/users/current", nil, nil)
	if err != nil {
		return "", err
	}
	return resp.UUID, nil
}

// loadOneChild loads only the named child, if it exists.
func (fs *customFileSystem) projectsLoadOne(parent inode, uuid, name string) (inode, error) {
	uuid, err := fs.defaultUUID(uuid)
	if err != nil {
		return nil, err
	}

	var contents CollectionList
	for _, subst := range []string{"/", fs.forwardSlashNameSubstitution} {
		contents = CollectionList{}
		err = fs.RequestAndDecode(&contents, "GET", "arvados/v1/groups/"+uuid+"/contents", nil, ResourceListParams{
			Count: "none",
			Order: "uuid",
			Filters: []Filter{
				{"name", "=", strings.Replace(name, subst, "/", -1)},
				{"uuid", "is_a", []string{"arvados#collection", "arvados#group"}},
				{"groups.group_class", "in", []string{"project", "filter"}},
			},
			Select: []string{"uuid", "name", "modified_at", "properties"},
		})
		if err != nil {
			return nil, err
		}
		if len(contents.Items) > 0 || fs.forwardSlashNameSubstitution == "/" || fs.forwardSlashNameSubstitution == "" || !strings.Contains(name, fs.forwardSlashNameSubstitution) {
			break
		}
		// If the requested name contains the configured "/"
		// replacement string and didn't match a
		// project/collection exactly, we'll try again with
		// "/" in its place, so a lookup of a munged name
		// works regardless of whether the directory listing
		// has been populated with escaped names.
		//
		// Note this doesn't handle items whose names contain
		// both "/" and the substitution string.
	}
	if len(contents.Items) == 0 {
		return nil, nil
	}
	coll := contents.Items[0]

	if strings.Contains(coll.UUID, "-j7d0g-") {
		// Group item was loaded into a Collection var -- but
		// we only need the Name and UUID anyway, so it's OK.
		return &hardlink{
			inode: fs.projectSingleton(coll.UUID, &Group{
				UUID:       coll.UUID,
				Name:       coll.Name,
				ModifiedAt: coll.ModifiedAt,
				Properties: coll.Properties,
			}),
			parent: parent,
			name:   coll.Name,
		}, nil
	} else if strings.Contains(coll.UUID, "-4zz18-") {
		return fs.newDeferredCollectionDir(parent, name, coll.UUID, coll.ModifiedAt, coll.Properties), nil
	} else {
		log.Printf("group contents: unrecognized UUID in response: %q", coll.UUID)
		return nil, ErrInvalidArgument
	}
}

func (fs *customFileSystem) projectsLoadAll(parent inode, uuid string) ([]inode, error) {
	uuid, err := fs.defaultUUID(uuid)
	if err != nil {
		return nil, err
	}

	pagesize := 100000
	var inodes []inode

	// When #17424 is resolved, remove the outer loop here and use
	// []string{"arvados#collection", "arvados#group"} directly as the uuid
	// filter.
	for _, class := range []string{"arvados#collection", "arvados#group"} {
		// Note: the "filters" slice's backing array might be reused
		// by append(filters,...) below. This isn't goroutine safe,
		// but all accesses are in the same goroutine, so it's OK.
		filters := []Filter{
			{"uuid", "is_a", class},
		}
		if class == "arvados#group" {
			filters = append(filters, Filter{"groups.group_class", "in", []string{"project", "filter"}})
		}

		params := ResourceListParams{
			Count:   "none",
			Filters: filters,
			Order:   "uuid",
			Select:  []string{"uuid", "name", "modified_at", "properties"},
			Limit:   &pagesize,
		}

		for {
			// The groups content endpoint returns
			// Collection and Group (project)
			// objects. This function only accesses the
			// UUID, Name, and ModifiedAt fields. Both
			// collections and groups have those fields,
			// so it is easier to just treat the
			// ObjectList that comes back as a
			// CollectionList.
			var resp CollectionList
			err = fs.RequestAndDecode(&resp, "GET", "arvados/v1/groups/"+uuid+"/contents", nil, params)
			if err != nil {
				return nil, err
			}
			if len(resp.Items) == 0 {
				break
			}
			for _, i := range resp.Items {
				if fs.forwardSlashNameSubstitution != "" {
					i.Name = strings.Replace(i.Name, "/", fs.forwardSlashNameSubstitution, -1)
				}
				if !permittedName(i.Name) {
					continue
				}
				if strings.Contains(i.UUID, "-j7d0g-") {
					inodes = append(inodes, fs.newProjectDir(parent, i.Name, i.UUID, &Group{
						UUID:       i.UUID,
						Name:       i.Name,
						ModifiedAt: i.ModifiedAt,
						Properties: i.Properties,
					}))
				} else if strings.Contains(i.UUID, "-4zz18-") {
					inodes = append(inodes, fs.newDeferredCollectionDir(parent, i.Name, i.UUID, i.ModifiedAt, i.Properties))
				} else {
					log.Printf("group contents: unrecognized UUID in response: %q", i.UUID)
					return nil, ErrInvalidArgument
				}
			}
			params.Filters = append(filters, Filter{"uuid", ">", resp.Items[len(resp.Items)-1].UUID})
		}
	}
	return inodes, nil
}

func (fs *customFileSystem) newProjectDir(parent inode, name, uuid string, proj *Group) inode {
	return &hardlink{inode: fs.projectSingleton(uuid, proj), parent: parent, name: name}
}

func (fs *customFileSystem) newDeferredCollectionDir(parent inode, name, uuid string, modTime time.Time, props map[string]interface{}) inode {
	if modTime.IsZero() {
		modTime = time.Now()
	}
	placeholder := &treenode{
		fs:     fs,
		parent: parent,
		inodes: nil,
		fileinfo: fileinfo{
			name:    name,
			modTime: modTime,
			mode:    0755 | os.ModeDir,
			sys:     func() interface{} { return &Collection{UUID: uuid, Name: name, ModifiedAt: modTime, Properties: props} },
		},
	}
	return &deferrednode{wrapped: placeholder, create: func() inode {
		node, err := fs.collectionSingleton(uuid)
		if err != nil {
			log.Printf("BUG: unhandled error: %s", err)
			return placeholder
		}
		return &hardlink{inode: node, parent: parent, name: name}
	}}
}
