// Copyright 2020-2021 Buf Technologies, 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 bufmodule

import (
	"context"
	"fmt"
	"io/ioutil"

	modulev1alpha1 "github.com/bufbuild/buf/internal/gen/proto/go/buf/alpha/module/v1alpha1"
	"github.com/bufbuild/buf/internal/pkg/encoding"
	"github.com/bufbuild/buf/internal/pkg/storage"
	"go.uber.org/multierr"
)

func modulePinLess(a ModulePin, b ModulePin) bool {
	return modulePinCompareTo(a, b) < 0
}

// return -1 if less
// return 1 if greater
// return 0 if equal
func modulePinCompareTo(a ModulePin, b ModulePin) int {
	if a == nil && b == nil {
		return 0
	}
	if a == nil && b != nil {
		return -1
	}
	if a != nil && b == nil {
		return 1
	}
	if a.Remote() < b.Remote() {
		return -1
	}
	if a.Remote() > b.Remote() {
		return 1
	}
	if a.Owner() < b.Owner() {
		return -1
	}
	if a.Owner() > b.Owner() {
		return 1
	}
	if a.Repository() < b.Repository() {
		return -1
	}
	if a.Repository() > b.Repository() {
		return 1
	}
	if a.Branch() < b.Branch() {
		return -1
	}
	if a.Branch() > b.Branch() {
		return 1
	}
	if a.Commit() < b.Commit() {
		return -1
	}
	if a.Commit() > b.Commit() {
		return 1
	}
	if a.Digest() < b.Digest() {
		return -1
	}
	if a.Digest() > b.Digest() {
		return 1
	}
	if a.CreateTime().Before(b.CreateTime()) {
		return -1
	}
	if a.CreateTime().After(b.CreateTime()) {
		return 1
	}
	return 0
}

func putModuleFileToBucket(ctx context.Context, module Module, path string, writeBucket storage.WriteBucket) (retErr error) {
	moduleFile, err := module.GetModuleFile(ctx, path)
	if err != nil {
		return err
	}
	defer func() {
		retErr = multierr.Append(retErr, moduleFile.Close())
	}()
	var copyOptions []storage.CopyOption
	if writeBucket.SetExternalPathSupported() {
		copyOptions = append(copyOptions, storage.CopyWithExternalPaths())
	}
	return storage.CopyReadObject(ctx, writeBucket, moduleFile, copyOptions...)
}

func moduleFileToProto(ctx context.Context, module Module, path string) (_ *modulev1alpha1.ModuleFile, retErr error) {
	protoModuleFile := &modulev1alpha1.ModuleFile{
		Path: path,
	}
	moduleFile, err := module.GetModuleFile(ctx, path)
	if err != nil {
		return nil, err
	}
	defer func() {
		retErr = multierr.Append(retErr, moduleFile.Close())
	}()
	protoModuleFile.Content, err = ioutil.ReadAll(moduleFile)
	if err != nil {
		return nil, err
	}
	return protoModuleFile, nil
}

// modulePins expected to be sorted and unique
func putDependencyModulePinsToBucket(
	ctx context.Context,
	writeBucket storage.WriteBucket,
	dependencyModulePins []ModulePin,
) (retErr error) {
	externalLockFile := newExternalLockFile(dependencyModulePins)
	yamlBytes, err := encoding.MarshalYAML(externalLockFile)
	if err != nil {
		return err
	}
	header := []byte("# Generated by buf. DO NOT EDIT.\n")
	return storage.PutPath(ctx, writeBucket, LockFilePath, append(header, yamlBytes...))
}

func getDependencyModulePinsForBucket(
	ctx context.Context,
	readBucket storage.ReadBucket,
) (_ []ModulePin, retErr error) {
	readObject, err := readBucket.Get(ctx, LockFilePath)
	if err != nil {
		if storage.IsNotExist(err) {
			// If the lock file doesn't exist, just return no dependencies.
			return nil, nil
		}
		return nil, err
	}
	defer func() {
		retErr = multierr.Append(retErr, readObject.Close())
	}()
	externalLockFile := &externalLockFile{}
	if err := encoding.NewYAMLDecoderStrict(readObject).Decode(externalLockFile); err != nil {
		return nil, err
	}
	return modulePinsForExternalLockFile(externalLockFile)
}

func newInvalidModuleOwnerStringError(s string) error {
	return fmt.Errorf("module owner %q is invalid: must be in the form remote/owner", s)
}

func newInvalidModuleIdentityStringError(s string) error {
	return fmt.Errorf("module identity %q is invalid: must be in the form remote/owner/repository", s)
}

func newInvalidModuleReferenceStringError(s string) error {
	return fmt.Errorf("module reference %q is invalid: must be in the form remote/owner/repository:branch or remote/owner/repository:commit", s)
}
