// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package btrfs

import (
	"bufio"
	"fmt"
	"os"
	"path"
	"path/filepath"
	"strconv"
	"strings"

	"github.com/prometheus/procfs"
	"github.com/prometheus/procfs/internal/fs"
	"github.com/prometheus/procfs/internal/util"
)

// FS represents the pseudo-filesystem sys, which provides an interface to
// kernel data structures.
type FS struct {
	sys *fs.FS
}

// NewDefaultFS returns a new Bcache using the default sys fs mount point. It will error
// if the mount point can't be read.
func NewDefaultFS() (FS, error) {
	return NewFS(fs.DefaultSysMountPoint)
}

// NewFS returns a new Btrfs filesystem using the given sys fs mount point. It will error
// if the mount point can't be read.
func NewFS(mountPoint string) (FS, error) {
	if strings.TrimSpace(mountPoint) == "" {
		mountPoint = fs.DefaultSysMountPoint
	}
	sys, err := fs.NewFS(mountPoint)
	if err != nil {
		return FS{}, err
	}
	return FS{&sys}, nil
}

// Stats retrieves Btrfs filesystem runtime statistics for each mounted Btrfs filesystem.
func (fs FS) Stats() ([]*Stats, error) {
	matches, err := filepath.Glob(fs.sys.Path("fs/btrfs/*-*"))
	if err != nil {
		return nil, err
	}

	stats := make([]*Stats, 0, len(matches))
	for _, uuidPath := range matches {
		s, err := GetStats(uuidPath)
		if err != nil {
			return nil, err
		}

		// Set the UUID from the path when it could not be retrieved from the filesystem.
		if s.UUID == "" {
			s.UUID = filepath.Base(uuidPath)
		}

		stats = append(stats, s)
	}

	return stats, nil
}

// GetStats collects all Btrfs statistics from sysfs.
func GetStats(uuidPath string) (*Stats, error) {
	r := &reader{path: uuidPath}
	s := r.readFilesystemStats()
	if r.err != nil {
		return nil, r.err
	}

	return s, nil
}

type reader struct {
	path     string
	err      error
	devCount int
}

// readFile reads a file relative to the path of the reader.
// Non-existing files are ignored.
func (r *reader) readFile(n string) string {
	b, err := util.SysReadFile(path.Join(r.path, n))
	if err != nil && !os.IsNotExist(err) {
		r.err = err
	}
	return strings.TrimSpace(string(b))
}

// readValues reads a number of numerical values into an uint64 slice.
func (r *reader) readValue(n string) (v uint64) {
	// Read value from file
	s := r.readFile(n)
	if r.err != nil {
		return
	}

	// Convert number
	v, _ = strconv.ParseUint(s, 10, 64)
	return
}

// listFiles returns a list of files for a directory of the reader.
func (r *reader) listFiles(p string) []string {
	files, err := os.ReadDir(path.Join(r.path, p))
	if err != nil {
		r.err = err
		return nil
	}

	names := make([]string, len(files))
	for i, f := range files {
		names[i] = f.Name()
	}
	return names
}

// readAllocationStats reads Btrfs allocation data for the current path.
func (r *reader) readAllocationStats(d string) (a *AllocationStats) {
	// Create a reader for this subdirectory
	sr := &reader{path: path.Join(r.path, d), devCount: r.devCount}

	// Get the stats
	a = &AllocationStats{
		// Read basic allocation stats
		MayUseBytes:      sr.readValue("bytes_may_use"),
		PinnedBytes:      sr.readValue("bytes_pinned"),
		ReadOnlyBytes:    sr.readValue("bytes_readonly"),
		ReservedBytes:    sr.readValue("bytes_reserved"),
		UsedBytes:        sr.readValue("bytes_used"),
		DiskUsedBytes:    sr.readValue("disk_used"),
		DiskTotalBytes:   sr.readValue("disk_total"),
		Flags:            sr.readValue("flags"),
		TotalBytes:       sr.readValue("total_bytes"),
		TotalPinnedBytes: sr.readValue("total_bytes_pinned"),
		Layouts:          sr.readLayouts(),
	}

	// Pass any error back
	r.err = sr.err

	return
}

// readLayouts reads all Btrfs layout statistics for the current path.
func (r *reader) readLayouts() map[string]*LayoutUsage {
	files, err := os.ReadDir(r.path)
	if err != nil {
		r.err = err
		return nil
	}

	m := make(map[string]*LayoutUsage)
	for _, f := range files {
		if f.IsDir() {
			m[f.Name()] = r.readLayout(f.Name())
		}
	}

	return m
}

// readLayout reads the Btrfs layout statistics for an allocation layout.
func (r *reader) readLayout(p string) (l *LayoutUsage) {
	l = new(LayoutUsage)
	l.TotalBytes = r.readValue(path.Join(p, "total_bytes"))
	l.UsedBytes = r.readValue(path.Join(p, "used_bytes"))
	l.Ratio = r.calcRatio(p)

	return
}

// calcRatio returns the calculated ratio for a layout mode.
func (r *reader) calcRatio(p string) float64 {
	switch p {
	case "single", "raid0":
		return 1
	case "dup", "raid1", "raid10":
		return 2
	case "raid1c3":
		return 3
	case "raid1c4":
		return 4
	case "raid5":
		return float64(r.devCount) / (float64(r.devCount) - 1)
	case "raid6":
		return float64(r.devCount) / (float64(r.devCount) - 2)
	default:
		return 0
	}
}

// readDeviceInfo returns the information for all devices associated with this filesystem.
func (r *reader) readDeviceInfo(d string) map[string]*Device {
	devs := r.listFiles(d)
	info := make(map[string]*Device, len(devs))
	for _, n := range devs {
		info[n] = &Device{
			Size: procfs.SectorSize * r.readValue("devices/"+n+"/size"),
		}
	}

	return info
}

// readFilesystemStats reads Btrfs statistics for a filesystem.
func (r *reader) readFilesystemStats() (s *Stats) {
	// First get disk info, and add it to reader
	devices := r.readDeviceInfo("devices")
	r.devCount = len(devices)

	s = &Stats{
		// Read basic filesystem information
		Label:          r.readFile("label"),
		UUID:           r.readFile("metadata_uuid"),
		Features:       r.listFiles("features"),
		CloneAlignment: r.readValue("clone_alignment"),
		NodeSize:       r.readValue("nodesize"),
		QuotaOverride:  r.readValue("quota_override"),
		SectorSize:     r.readValue("sectorsize"),

		// Device info
		Devices: devices,

		// Read allocation data
		Allocation: Allocation{
			GlobalRsvReserved: r.readValue("allocation/global_rsv_reserved"),
			GlobalRsvSize:     r.readValue("allocation/global_rsv_size"),
			Data:              r.readAllocationStats("allocation/data"),
			Metadata:          r.readAllocationStats("allocation/metadata"),
			System:            r.readAllocationStats("allocation/system"),
		},

		// Read commit stats data
		CommitStats: r.readCommitStats("commit_stats"),
	}
	return
}

// readCommitStats returns the commit_stats information for commit stats metrics.
func (r *reader) readCommitStats(p string) CommitStats {
	stats := CommitStats{}

	f, err := os.Open(path.Join(r.path, p))
	if err != nil {
		// if commit_stats not found. maybe btrfs version < 6.0
		if !os.IsNotExist(err) {
			r.err = err
		}
		return stats
	}
	defer f.Close()

	scanner := bufio.NewScanner(f)

	for scanner.Scan() {
		line := scanner.Text()
		parts := strings.Fields(scanner.Text())
		// require  <key> <value>
		if len(parts) != 2 {
			r.err = fmt.Errorf("invalid commit_stats line %q", line)
			return stats
		}

		value, err := strconv.ParseUint(parts[1], 10, 64)
		if err != nil {
			r.err = fmt.Errorf("error parsing commit_stats line: %w", err)
			return stats
		}

		switch metricName := parts[0]; metricName {
		case "commits":
			stats.Commits = value
		case "last_commit_ms":
			stats.LastCommitMs = value
		case "max_commit_ms":
			stats.MaxCommitMs = value
		case "total_commit_ms":
			stats.TotalCommitMs = value
		default:
			continue
		}
	}

	if err := scanner.Err(); err != nil {
		r.err = fmt.Errorf("error scanning commit_stats file: %w", err)
		return stats
	}

	return stats
}
