//go:build linux

/*
   Copyright The containerd 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 devmapper

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"strconv"

	"github.com/containerd/errdefs"
	bolt "go.etcd.io/bbolt"
	errbolt "go.etcd.io/bbolt/errors"
)

type (
	// DeviceInfoCallback is a callback used for device updates
	DeviceInfoCallback func(deviceInfo *DeviceInfo) error
)

type deviceIDState byte

const (
	deviceFree deviceIDState = iota
	deviceTaken
	deviceFaulty
)

// Bucket names
var (
	devicesBucketName  = []byte("devices")    // Contains thin devices metadata <device_name>=<DeviceInfo>
	deviceIDBucketName = []byte("device_ids") // Tracks used device ids <device_id_[0..maxDeviceID)>=<byte_[0/1]>
)

var (
	// ErrNotFound represents an error returned when object not found in meta store
	ErrNotFound = errdefs.ErrNotFound
	// ErrAlreadyExists represents an error returned when object can't be duplicated in meta store
	ErrAlreadyExists = errdefs.ErrAlreadyExists
)

// PoolMetadata keeps device info for the given thin-pool device, generates next available device ids,
// and tracks devmapper transaction numbers
type PoolMetadata struct {
	db *bolt.DB
}

// NewPoolMetadata creates new or opens existing pool metadata database
func NewPoolMetadata(dbfile string) (*PoolMetadata, error) {
	db, err := bolt.Open(dbfile, 0600, nil)
	if err != nil {
		return nil, err
	}

	metadata := &PoolMetadata{db: db}
	if err := metadata.ensureDatabaseInitialized(); err != nil {
		return nil, fmt.Errorf("failed to initialize database: %w", err)
	}

	return metadata, nil
}

// ensureDatabaseInitialized creates buckets required for metadata store in order
// to avoid bucket existence checks across the code
func (m *PoolMetadata) ensureDatabaseInitialized() error {
	return m.db.Update(func(tx *bolt.Tx) error {
		if _, err := tx.CreateBucketIfNotExists(devicesBucketName); err != nil {
			return err
		}

		if _, err := tx.CreateBucketIfNotExists(deviceIDBucketName); err != nil {
			return err
		}

		return nil
	})
}

// AddDevice saves device info to database.
func (m *PoolMetadata) AddDevice(ctx context.Context, info *DeviceInfo) error {
	err := m.db.Update(func(tx *bolt.Tx) error {
		devicesBucket := tx.Bucket(devicesBucketName)

		// Make sure device name is unique. If there is already a device with the same name,
		// but in Faulty state, give it a try with another devmapper device ID.
		// See https://github.com/containerd/containerd/pull/3436 for more context.
		var existing DeviceInfo
		if err := getObject(devicesBucket, info.Name, &existing); err == nil && existing.State != Faulty {
			return fmt.Errorf("device %q is already there %+v: %w", info.Name, existing, ErrAlreadyExists)
		}

		// Find next available device ID
		deviceID, err := getNextDeviceID(tx)
		if err != nil {
			return err
		}

		info.DeviceID = deviceID

		return putObject(devicesBucket, info.Name, info, true)
	})

	if err != nil {
		return fmt.Errorf("failed to save metadata for device %q (parent: %q): %w", info.Name, info.ParentName, err)
	}

	return nil
}

// ChangeDeviceState changes the device state given the device name in devices bucket.
func (m *PoolMetadata) ChangeDeviceState(ctx context.Context, name string, state DeviceState) error {
	return m.UpdateDevice(ctx, name, func(deviceInfo *DeviceInfo) error {
		deviceInfo.State = state
		return nil
	})
}

// MarkFaulty marks the given device and corresponding devmapper device ID as faulty.
// The snapshotter might attempt to recreate a device in 'Faulty' state with another devmapper ID in
// subsequent calls, and in case of success its status will be changed to 'Created' or 'Activated'.
// The devmapper dev ID will remain in 'deviceFaulty' state until manually handled by a user.
func (m *PoolMetadata) MarkFaulty(ctx context.Context, name string) error {
	return m.db.Update(func(tx *bolt.Tx) error {
		var (
			device    = DeviceInfo{}
			devBucket = tx.Bucket(devicesBucketName)
		)

		if err := getObject(devBucket, name, &device); err != nil {
			return err
		}

		device.State = Faulty

		if err := putObject(devBucket, name, &device, true); err != nil {
			return err
		}

		return markDeviceID(tx, device.DeviceID, deviceFaulty)
	})
}

// getNextDeviceID finds the next free device ID by taking a cursor
// through the deviceIDBucketName bucket and finding the next sequentially
// unassigned ID. Device ID state is marked by a byte deviceFree or
// deviceTaken. Low device IDs will be reused sooner.
func getNextDeviceID(tx *bolt.Tx) (uint32, error) {
	bucket := tx.Bucket(deviceIDBucketName)
	cursor := bucket.Cursor()

	// Check if any device id can be reused.
	// Bolt stores its keys in byte-sorted order within a bucket.
	// This makes sequential iteration extremely fast.
	for key, taken := cursor.First(); key != nil; key, taken = cursor.Next() {
		isFree := taken[0] == byte(deviceFree)
		if !isFree {
			continue
		}

		parsedID, err := strconv.ParseUint(string(key), 10, 32)
		if err != nil {
			return 0, err
		}

		id := uint32(parsedID)
		if err := markDeviceID(tx, id, deviceTaken); err != nil {
			return 0, err
		}

		return id, nil
	}

	// Try allocate new device ID
	seq, err := bucket.NextSequence()
	if err != nil {
		return 0, err
	}

	if seq >= maxDeviceID {
		return 0, errors.New("dm-meta: couldn't find free device key")
	}

	id := uint32(seq)
	if err := markDeviceID(tx, id, deviceTaken); err != nil {
		return 0, err
	}

	return id, nil
}

// markDeviceID marks a device as deviceFree or deviceTaken
func markDeviceID(tx *bolt.Tx, deviceID uint32, state deviceIDState) error {
	var (
		bucket = tx.Bucket(deviceIDBucketName)
		key    = strconv.FormatUint(uint64(deviceID), 10)
		value  = []byte{byte(state)}
	)

	if err := bucket.Put([]byte(key), value); err != nil {
		return fmt.Errorf("failed to free device id %q: %w", key, err)
	}

	return nil
}

// UpdateDevice updates device info in metadata store.
// The callback should be used to indicate whether device info update was successful or not.
// An error returned from the callback will rollback the update transaction in the database.
// Name and Device ID are not allowed to change.
func (m *PoolMetadata) UpdateDevice(ctx context.Context, name string, fn DeviceInfoCallback) error {
	return m.db.Update(func(tx *bolt.Tx) error {
		var (
			device = &DeviceInfo{}
			bucket = tx.Bucket(devicesBucketName)
		)

		if err := getObject(bucket, name, device); err != nil {
			return err
		}

		// Don't allow changing these values, keep things in sync with devmapper
		name := device.Name
		devID := device.DeviceID

		if err := fn(device); err != nil {
			return err
		}

		if name != device.Name {
			return fmt.Errorf("failed to update device info, name didn't match: %q %q", name, device.Name)
		}

		if devID != device.DeviceID {
			return fmt.Errorf("failed to update device info, device id didn't match: %d %d", devID, device.DeviceID)
		}

		return putObject(bucket, name, device, true)
	})
}

// GetDevice retrieves device info by name from database
func (m *PoolMetadata) GetDevice(ctx context.Context, name string) (*DeviceInfo, error) {
	var (
		dev DeviceInfo
		err error
	)

	err = m.db.View(func(tx *bolt.Tx) error {
		bucket := tx.Bucket(devicesBucketName)
		return getObject(bucket, name, &dev)
	})

	return &dev, err
}

// RemoveDevice removes device info from store.
func (m *PoolMetadata) RemoveDevice(ctx context.Context, name string) error {
	return m.db.Update(func(tx *bolt.Tx) error {
		var (
			device = &DeviceInfo{}
			bucket = tx.Bucket(devicesBucketName)
		)

		if err := getObject(bucket, name, device); err != nil {
			return err
		}

		if err := bucket.Delete([]byte(name)); err != nil {
			return fmt.Errorf("failed to delete device info for %q: %w", name, err)
		}

		return markDeviceID(tx, device.DeviceID, deviceFree)
	})
}

// WalkDevices walks all devmapper devices in metadata store and invokes the callback with device info.
// The provided callback function must not modify the bucket.
func (m *PoolMetadata) WalkDevices(ctx context.Context, cb func(info *DeviceInfo) error) error {
	return m.db.View(func(tx *bolt.Tx) error {
		bucket := tx.Bucket(devicesBucketName)
		return bucket.ForEach(func(key, value []byte) error {
			device := &DeviceInfo{}
			if err := json.Unmarshal(value, device); err != nil {
				return fmt.Errorf("failed to unmarshal %s: %w", key, err)
			}

			return cb(device)
		})
	})
}

// GetDeviceNames retrieves the list of device names currently stored in database
func (m *PoolMetadata) GetDeviceNames(ctx context.Context) ([]string, error) {
	var (
		names []string
		err   error
	)

	err = m.db.View(func(tx *bolt.Tx) error {
		bucket := tx.Bucket(devicesBucketName)
		return bucket.ForEach(func(k, _ []byte) error {
			names = append(names, string(k))
			return nil
		})
	})

	if err != nil {
		return nil, err
	}

	return names, nil
}

// Close closes metadata store
func (m *PoolMetadata) Close() error {
	if err := m.db.Close(); err != nil && err != errbolt.ErrDatabaseNotOpen {
		return err
	}

	return nil
}

func putObject(bucket *bolt.Bucket, key string, obj any, overwrite bool) error {
	keyBytes := []byte(key)

	if !overwrite && bucket.Get(keyBytes) != nil {
		return fmt.Errorf("object with key %q already exists", key)
	}

	data, err := json.Marshal(obj)
	if err != nil {
		return fmt.Errorf("failed to marshal object with key %q: %w", key, err)
	}

	if err := bucket.Put(keyBytes, data); err != nil {
		return fmt.Errorf("failed to insert object with key %q: %w", key, err)
	}

	return nil
}

func getObject(bucket *bolt.Bucket, key string, obj any) error {
	data := bucket.Get([]byte(key))
	if data == nil {
		return ErrNotFound
	}

	if obj != nil {
		if err := json.Unmarshal(data, obj); err != nil {
			return fmt.Errorf("failed to unmarshal object with key %q: %w", key, err)
		}
	}

	return nil
}
