/*
 * MinIO Cloud Storage, (C) 2020 MinIO, Inc.
 *
 * 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 cmd

import (
	"context"
	"encoding/binary"
	"errors"
	"fmt"
	"path"
	"time"

	"github.com/minio/minio/cmd/logger"
)

const (
	legacyBucketObjectLockEnabledConfigFile = "object-lock-enabled.json"
	legacyBucketObjectLockEnabledConfig     = `{"x-amz-bucket-object-lock-enabled":true}`

	bucketMetadataFile    = ".metadata.bin"
	bucketMetadataFormat  = 1
	bucketMetadataVersion = 1
)

//go:generate msgp -file $GOFILE -unexported

// bucketMetadata contains bucket metadata.
// When adding/removing fields, regenerate the marshal code using the go generate above.
// Only changing meaning of fields requires a version bump.
// bucketMetadataFormat refers to the format.
// bucketMetadataVersion can be used to track a rolling upgrade of a field.
type bucketMetadata struct {
	Name        string
	Created     time.Time
	LockEnabled bool
}

// newBucketMetadata creates bucketMetadata with the supplied name and Created to Now.
func newBucketMetadata(name string) bucketMetadata {
	return bucketMetadata{
		Name:    name,
		Created: UTCNow(),
	}
}

// loadBucketMeta loads the metadata of bucket by name from ObjectLayer o.
// If an error is returned the returned metadata will be default initialized.
func loadBucketMeta(ctx context.Context, o ObjectLayer, name string) (bucketMetadata, error) {
	b := newBucketMetadata(name)
	configFile := path.Join(bucketConfigPrefix, name, bucketMetadataFile)
	data, err := readConfig(ctx, o, configFile)
	if err != nil {
		return b, err
	}
	if len(data) <= 4 {
		return b, fmt.Errorf("loadBucketMetadata: no data")
	}
	// Read header
	switch binary.LittleEndian.Uint16(data[0:2]) {
	case bucketMetadataFormat:
	default:
		return b, fmt.Errorf("loadBucketMetadata: unknown format: %d", binary.LittleEndian.Uint16(data[0:2]))
	}
	switch binary.LittleEndian.Uint16(data[2:4]) {
	case bucketMetadataVersion:
	default:
		return b, fmt.Errorf("loadBucketMetadata: unknown version: %d", binary.LittleEndian.Uint16(data[2:4]))
	}

	// OK, parse data.
	_, err = b.UnmarshalMsg(data[4:])
	return b, err
}

var errMetaDataConverted = errors.New("metadata converted")

// loadBucketMetadata loads and migrates to bucket metadata.
func loadBucketMetadata(ctx context.Context, objectAPI ObjectLayer, bucket string) (bucketMetadata, error) {
	meta, err := loadBucketMeta(ctx, objectAPI, bucket)
	if err == nil {
		return meta, nil
	}

	if err != errConfigNotFound {
		return meta, err
	}

	// Control here means old bucket without bucket metadata. Hence we migrate existing settings.
	if err = meta.convertLegacyLockconfig(ctx, objectAPI); err != nil {
		return meta, err
	}

	return meta, errMetaDataConverted
}

func (b *bucketMetadata) convertLegacyLockconfig(ctx context.Context, objectAPI ObjectLayer) error {
	configFile := path.Join(bucketConfigPrefix, b.Name, legacyBucketObjectLockEnabledConfigFile)

	save := func() error {
		if err := b.save(ctx, objectAPI); err != nil {
			return err
		}

		if err := deleteConfig(ctx, objectAPI, configFile); err != nil && !errors.Is(err, errConfigNotFound) {
			logger.LogIf(ctx, err)
		}
		return nil
	}

	configData, err := readConfig(ctx, objectAPI, configFile)
	if err != nil {
		if !errors.Is(err, errConfigNotFound) {
			return err
		}

		return save()
	}

	if string(configData) != legacyBucketObjectLockEnabledConfig {
		return fmt.Errorf("content mismatch in config file %v", configFile)
	}

	b.LockEnabled = true
	return save()
}

// save config to supplied ObjectLayer o.
func (b *bucketMetadata) save(ctx context.Context, o ObjectLayer) error {
	data := make([]byte, 4, b.Msgsize()+4)
	// Put header
	binary.LittleEndian.PutUint16(data[0:2], bucketMetadataFormat)
	binary.LittleEndian.PutUint16(data[2:4], bucketMetadataVersion)

	// Add data
	data, err := b.MarshalMsg(data)
	if err != nil {
		return err
	}
	configFile := path.Join(bucketConfigPrefix, b.Name, bucketMetadataFile)
	return saveConfig(ctx, o, configFile, data)
}

// removeBucketMeta bucket metadata.
// If config does not exist no error is returned.
func removeBucketMeta(ctx context.Context, obj ObjectLayer, bucket string) error {
	configFile := path.Join(bucketConfigPrefix, bucket, bucketMetadataFile)
	if err := deleteConfig(ctx, obj, configFile); err != nil && err != errConfigNotFound {
		return err
	}
	return nil
}