// Copyright (c) 2021 Gitpod GmbH. All rights reserved.
// Licensed under the GNU Affero General Public License (AGPL).
// See License.AGPL.txt in the project root for license information.

package quota

import (
	"fmt"
	"os/exec"
	"strconv"
	"strings"
	"sync"
)

type xfsQuotaExec func(dir, command string) (output string, err error)

func defaultXfsQuotaExec(dir, command string) (output string, err error) {
	out, err := exec.Command("xfs_quota", "-x", "-c", command, dir).CombinedOutput()
	if err != nil {
		return "", fmt.Errorf("xfs_quota error: %s: %v", string(out), err)
	}
	return string(out), nil
}

const (
	prjidLow = 1000
	prjidHi  = 10000
)

type XFS struct {
	Dir string

	exec xfsQuotaExec

	projectIDs map[int]struct{}
	mu         sync.Mutex
}

func NewXFS(path string) (*XFS, error) {
	res := &XFS{
		Dir:        path,
		projectIDs: make(map[int]struct{}),
		exec:       defaultXfsQuotaExec,
	}

	// Note: if the underlying filesystem does not support XFS project quota,
	//       getUsedProjectIDs will fail, hence the NewXFS call will fail.
	prjIDs, err := res.getUsedProjectIDs()
	if err != nil {
		return nil, err
	}
	for _, prjID := range prjIDs {
		res.projectIDs[prjID] = struct{}{}
	}

	return res, nil
}

// getUsedProjectIDs lists all project IDs used on the filesystem
func (xfs *XFS) getUsedProjectIDs() ([]int, error) {
	out, err := xfs.exec(xfs.Dir, "report -N")
	if err != nil {
		return nil, err
	}

	var res []int
	for _, l := range strings.Split(out, "\n") {
		fields := strings.Fields(l)
		if len(fields) < 2 {
			continue
		}

		prjID, err := strconv.Atoi(strings.TrimPrefix(fields[0], "#"))
		if err != nil {
			continue
		}

		used, err := strconv.Atoi(strings.TrimSpace(fields[1]))
		if err != nil || used == 0 {
			continue
		}

		res = append(res, prjID)
	}
	return res, nil
}

// SetQuota sets the quota for a path
func (xfs *XFS) SetQuota(path string, quota Size, isHard bool) (projectID int, err error) {
	xfs.mu.Lock()
	var (
		prjID = prjidLow
		found bool
	)
	for ; prjID < prjidHi; prjID++ {
		_, exists := xfs.projectIDs[prjID]
		if !exists {
			found = true
			xfs.projectIDs[prjID] = struct{}{}
			break
		}
	}
	xfs.mu.Unlock()
	if !found {
		return 0, fmt.Errorf("no free projectID found")
	}

	defer func() {
		if err != nil {
			xfs.mu.Lock()
			delete(xfs.projectIDs, prjID)
			xfs.mu.Unlock()
		}
	}()

	_, err = xfs.SetQuotaWithPrjId(path, quota, prjID, isHard)
	if err != nil {
		return 0, err
	}

	return prjID, nil
}

func (xfs *XFS) SetQuotaWithPrjId(path string, quota Size, prjID int, isHard bool) (projectID int, err error) {
	_, err = xfs.exec(xfs.Dir, fmt.Sprintf("project -s -d 1 -p %s %d", path, prjID))
	if err != nil {
		return 0, err
	}

	if isHard {
		_, err = xfs.exec(xfs.Dir, fmt.Sprintf("limit -p bhard=%d %d", quota, prjID))
	} else {
		_, err = xfs.exec(xfs.Dir, fmt.Sprintf("limit -p bsoft=%d %d", quota, prjID))
	}

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

// RegisterProject tells this implementation that a projectID is already in use
func (xfs *XFS) RegisterProject(prjID int) {
	xfs.mu.Lock()
	defer xfs.mu.Unlock()

	xfs.projectIDs[prjID] = struct{}{}
}

// RemoveQuota removes the limitation for a project/path and frees the projectID
func (xfs *XFS) RemoveQuota(projectID int) error {
	_, err := xfs.exec(xfs.Dir, fmt.Sprintf("limit -p bsoft=0 bhard=0 %d", projectID))
	if err != nil {
		return err
	}
	xfs.mu.Lock()
	delete(xfs.projectIDs, projectID)
	xfs.mu.Unlock()
	return nil
}

// GetProjectUseCount returns the number of projectIDs in use
func (xfs *XFS) GetProjectUseCount() int {
	xfs.mu.Lock()
	defer xfs.mu.Unlock()

	return len(xfs.projectIDs)
}
