/*
   Copyright 2020 Docker Compose CLI 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 compose

import (
	"context"
	"errors"
	"os"
	"strings"

	"github.com/compose-spec/compose-go/v2/cli"
	"github.com/compose-spec/compose-go/v2/loader"
	"github.com/compose-spec/compose-go/v2/types"

	"github.com/docker/compose/v5/pkg/api"
	"github.com/docker/compose/v5/pkg/remote"
	"github.com/docker/compose/v5/pkg/utils"
)

// LoadProject implements api.Compose.LoadProject
// It loads and validates a Compose project from configuration files.
func (s *composeService) LoadProject(ctx context.Context, options api.ProjectLoadOptions) (*types.Project, error) {
	// Setup remote loaders (Git, OCI)
	remoteLoaders := s.createRemoteLoaders(options)

	projectOptions, err := s.buildProjectOptions(options, remoteLoaders)
	if err != nil {
		return nil, err
	}

	// Register all user-provided listeners (e.g., for metrics collection)
	for _, listener := range options.LoadListeners {
		if listener != nil {
			projectOptions.WithListeners(listener)
		}
	}

	if options.Compatibility || utils.StringToBool(projectOptions.Environment[api.ComposeCompatibility]) {
		api.Separator = "_"
	}

	project, err := projectOptions.LoadProject(ctx)
	if err != nil {
		return nil, err
	}

	// Post-processing: service selection, environment resolution, etc.
	project, err = s.postProcessProject(project, options)
	if err != nil {
		return nil, err
	}

	return project, nil
}

// createRemoteLoaders creates Git and OCI remote loaders if not in offline mode
func (s *composeService) createRemoteLoaders(options api.ProjectLoadOptions) []loader.ResourceLoader {
	if options.Offline {
		return nil
	}
	git := remote.NewGitRemoteLoader(s.dockerCli, options.Offline)
	oci := remote.NewOCIRemoteLoader(s.dockerCli, options.Offline, options.OCI)
	return []loader.ResourceLoader{git, oci}
}

// buildProjectOptions constructs compose-go ProjectOptions from API options
func (s *composeService) buildProjectOptions(options api.ProjectLoadOptions, remoteLoaders []loader.ResourceLoader) (*cli.ProjectOptions, error) {
	opts := []cli.ProjectOptionsFn{
		cli.WithWorkingDirectory(options.WorkingDir),
		cli.WithOsEnv,
	}

	// Add PWD if not present
	if _, present := os.LookupEnv("PWD"); !present {
		if pwd, err := os.Getwd(); err == nil {
			opts = append(opts, cli.WithEnv([]string{"PWD=" + pwd}))
		}
	}

	// Add remote loaders
	for _, r := range remoteLoaders {
		opts = append(opts, cli.WithResourceLoader(r))
	}

	opts = append(opts,
		// Load PWD/.env if present and no explicit --env-file has been set
		cli.WithEnvFiles(options.EnvFiles...),
		// read dot env file to populate project environment
		cli.WithDotEnv,
		// get compose file path set by COMPOSE_FILE
		cli.WithConfigFileEnv,
		// if none was selected, get default compose.yaml file from current dir or parent folder
		cli.WithDefaultConfigPath,
		// .. and then, a project directory != PWD maybe has been set so let's load .env file
		cli.WithEnvFiles(options.EnvFiles...), //nolint:gocritic // intentionally applying cli.WithEnvFiles twice.
		cli.WithDotEnv,                        //nolint:gocritic // intentionally applying cli.WithDotEnv twice.
		// eventually COMPOSE_PROFILES should have been set
		cli.WithDefaultProfiles(options.Profiles...),
		cli.WithName(options.ProjectName),
	)

	return cli.NewProjectOptions(options.ConfigPaths, append(options.ProjectOptionsFns, opts...)...)
}

// postProcessProject applies post-loading transformations to the project
func (s *composeService) postProcessProject(project *types.Project, options api.ProjectLoadOptions) (*types.Project, error) {
	if project.Name == "" {
		return nil, errors.New("project name can't be empty. Use ProjectName option to set a valid name")
	}

	project, err := project.WithServicesEnabled(options.Services...)
	if err != nil {
		return nil, err
	}

	// Add custom labels
	for name, s := range project.Services {
		s.CustomLabels = map[string]string{
			api.ProjectLabel:     project.Name,
			api.ServiceLabel:     name,
			api.VersionLabel:     api.ComposeVersion,
			api.WorkingDirLabel:  project.WorkingDir,
			api.ConfigFilesLabel: strings.Join(project.ComposeFiles, ","),
			api.OneoffLabel:      "False",
		}
		if len(options.EnvFiles) != 0 {
			s.CustomLabels[api.EnvironmentFileLabel] = strings.Join(options.EnvFiles, ",")
		}
		project.Services[name] = s
	}

	project, err = project.WithSelectedServices(options.Services)
	if err != nil {
		return nil, err
	}

	// Remove unnecessary resources if not All
	if !options.All {
		project = project.WithoutUnnecessaryResources()
	}

	return project, nil
}
