// Copyright 2020-2025 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"
	"errors"
	"fmt"
	"net/url"

	"github.com/bufbuild/buf/private/pkg/slicesext"
	"github.com/bufbuild/buf/private/pkg/syserror"
)

var (
	// NopUploader is a no-op Uploader.
	NopUploader Uploader = nopUploader{}
)

// Uploader uploads ModuleSets.
type Uploader interface {
	// Upload uploads the given ModuleSet.
	Upload(ctx context.Context, moduleSet ModuleSet, options ...UploadOption) ([]Commit, error)
}

// UploadOption is an option for an Upload.
type UploadOption func(*uploadOptions)

// UploadWithLabels returns a new UploadOption that adds the given labels.
//
// This can be called multiple times. The unique result set of labels will be used.
// We only ever allow one of labels or tags to be set.
func UploadWithLabels(labels ...string) UploadOption {
	return func(uploadOptions *uploadOptions) {
		uploadOptions.labels = append(uploadOptions.labels, labels...)
	}
}

// UploadWithTags returns a new UploadOption that adds the given tags. This is handled
// separately from labels because we need to resolve the default label(s) when uploading.
//
// This can be called multiple times. The unique result set of tags will be used.
// We only ever allow one of labels or tags to be set.
func UploadWithTags(tags ...string) UploadOption {
	return func(uploadOptions *uploadOptions) {
		uploadOptions.tags = append(uploadOptions.tags, tags...)
	}
}

// UploadWithCreateIfNotExist returns a new UploadOption that will result in the
// Modules being created on the registry with the given visibility and default label if they do not exist.
// If the default label name is not provided, the module will be created with the default label "main".
func UploadWithCreateIfNotExist(createModuleVisibility ModuleVisibility, createDefaultLabel string) UploadOption {
	return func(uploadOptions *uploadOptions) {
		uploadOptions.createIfNotExist = true
		uploadOptions.createModuleVisibility = createModuleVisibility
		uploadOptions.createDefaultLabel = createDefaultLabel
	}
}

// UploadWithSourceControlURL returns a new UploadOption that will set the source control
// url for the module contents uploaded.
func UploadWithSourceControlURL(sourceControlURL string) UploadOption {
	return func(uploadOptions *uploadOptions) {
		uploadOptions.sourceControlURL = sourceControlURL
	}
}

// UploadWithExcludeUnnamed returns a new UploadOption that will exclude unnamed modules.
func UploadWithExcludeUnnamed() UploadOption {
	return func(uploadOptions *uploadOptions) {
		uploadOptions.excludeUnnamed = true
	}
}

// UploadOptions are the possible options for upload.
//
// This is used by Uploader implementations.
type UploadOptions interface {
	// Labels returns the unique and sorted set of labels to add.
	// Labels are set using the `--label` flag when calling `buf push` and represent the
	// labels that are set when uploading module content.
	Labels() []string
	// CreateIfNotExist says to create Modules if they do not exist on the registry.
	CreateIfNotExist() bool
	// CreateModuleVisibility returns the visibility to create Modules with.
	//
	// Will always be present if CreateIfNotExist() is true.
	CreateModuleVisibility() ModuleVisibility
	// CreateDefaultLabel returns the default label to create Modules with. If this is an
	// empty string, then the Modules will be created with default label "main".
	CreateDefaultLabel() string
	// Tags returns unique and sorted set of tags to be added as labels.
	// Tags are set using the `--tag` flag when calling `buf push`, and represent labels
	// that are set **in addition to** the default label when uploading module content.
	//
	// The `--tag` flag is a legacy flag that we are continuing supporting. We need to
	// handle tags differently from labels when uploading because we need to resolve the
	// default label for each module.
	//
	// We disallow the use of `--tag` when the modules we are uploading to do not all have
	// the same default label.
	Tags() []string
	// SourceControlURL returns the source control URL set by the user for the module
	// contents uploaded. We set the same source control URL for all module contents.
	SourceControlURL() string
	// ExcludeUnnamed returns whether to exclude unnamed modules.
	ExcludeUnnamed() bool

	isUploadOptions()
}

// NewUploadOptions returns a new UploadOptions.
func NewUploadOptions(options []UploadOption) (UploadOptions, error) {
	uploadOptions := newUploadOptions()
	for _, option := range options {
		option(uploadOptions)
	}
	if err := uploadOptions.validate(); err != nil {
		return nil, err
	}
	return uploadOptions, nil
}

// *** PRIVATE ***

type nopUploader struct{}

func (nopUploader) Upload(context.Context, ModuleSet, ...UploadOption) ([]Commit, error) {
	return nil, errors.New("unimplemented: no-op Uploader called")
}

type uploadOptions struct {
	labels                 []string
	tags                   []string
	createIfNotExist       bool
	createModuleVisibility ModuleVisibility
	createDefaultLabel     string
	sourceControlURL       string
	excludeUnnamed         bool
}

func newUploadOptions() *uploadOptions {
	return &uploadOptions{}
}

func (u *uploadOptions) Labels() []string {
	return slicesext.ToUniqueSorted(u.labels)
}

func (u *uploadOptions) Tags() []string {
	return slicesext.ToUniqueSorted(u.tags)
}

func (u *uploadOptions) CreateIfNotExist() bool {
	return u.createIfNotExist
}

func (u *uploadOptions) CreateModuleVisibility() ModuleVisibility {
	return u.createModuleVisibility
}

func (u *uploadOptions) CreateDefaultLabel() string {
	return u.createDefaultLabel
}

func (u *uploadOptions) SourceControlURL() string {
	return u.sourceControlURL
}

func (u *uploadOptions) ExcludeUnnamed() bool {
	return u.excludeUnnamed
}

func (u *uploadOptions) validate() error {
	if u.createIfNotExist && u.createModuleVisibility == 0 {
		return errors.New("must set a valid ModuleVisibility if CreateIfNotExist was specified")
	}
	// We validate that only one of labels or tags is set.
	// This is enforced at the flag level, so if more than one is set, we return a syserror.
	if len(u.labels) > 0 && len(u.tags) > 0 {
		return syserror.New("cannot set both labels and tags")
	}
	if u.sourceControlURL != "" {
		if _, err := url.Parse(u.sourceControlURL); err != nil {
			return fmt.Errorf("must set a valid url for the source control url: %w", err)
		}
	}
	return nil
}

func (*uploadOptions) isUploadOptions() {}
