// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package getproviders

import (
	"context"
	"fmt"
	"sync"

	"github.com/apparentlymart/go-versions/versions"
	"github.com/opentofu/opentofu/internal/addrs"
)

// FilesystemMirrorSource is a source that reads providers and their metadata
// from a directory prefix in the local filesystem.
type FilesystemMirrorSource struct {
	baseDir string

	// allPackages caches the result of scanning the baseDir for all available
	// packages on the first call that needs package availability information,
	// to avoid re-scanning the filesystem on subsequent operations.
	allPackages map[addrs.Provider]PackageMetaList

	lock sync.Mutex
}

var _ Source = (*FilesystemMirrorSource)(nil)

// NewFilesystemMirrorSource constructs and returns a new filesystem-based
// mirror source with the given base directory.
func NewFilesystemMirrorSource(_ context.Context, baseDir string) *FilesystemMirrorSource {
	return &FilesystemMirrorSource{
		baseDir: baseDir,
	}
}

// AvailableVersions scans the directory structure under the source's base
// directory for locally-mirrored packages for the given provider, returning
// a list of version numbers for the providers it found.
func (s *FilesystemMirrorSource) AvailableVersions(ctx context.Context, provider addrs.Provider) (VersionList, Warnings, error) {
	// s.allPackages is populated if scanAllVersions succeeds
	err := s.scanAllVersions()
	if err != nil {
		return nil, nil, err
	}

	// There might be multiple packages for a given version in the filesystem,
	// but the contract here is to return distinct versions so we'll dedupe
	// them first, then sort them, and then return them.
	versionsMap := make(map[Version]struct{})
	for _, m := range s.allPackages[provider] {
		versionsMap[m.Version] = struct{}{}
	}
	ret := make(VersionList, 0, len(versionsMap))
	for v := range versionsMap {
		ret = append(ret, v)
	}
	ret.Sort()
	// Check the existence of provider version 0.0.0 in the filesystem and warn the user about it
	// If it exists, it will be the first element in the sorted list
	var warnings Warnings
	if len(ret) > 0 && ret[0] == versions.Unspecified {
		warning := fmt.Sprintf("Provider %s has an unspecified (0.0.0) version available in the filesystem mirror, source at %s. It will not be used. \n"+
			"If the version 0.0.0 is intended to represent a non-published provider, consider using dev_overrides - https://opentofu.org/docs/cli/config/config-file/#development-overrides-for-provider-developers",
			provider, s.baseDir)
		warnings = append(warnings, warning)
	}
	return ret, warnings, nil
}

// PackageMeta checks to see if the source's base directory contains a
// local copy of the distribution package for the given provider version on
// the given target, and returns the metadata about it if so.
func (s *FilesystemMirrorSource) PackageMeta(ctx context.Context, provider addrs.Provider, version Version, target Platform) (PackageMeta, error) {
	// s.allPackages is populated if scanAllVersions succeeds
	err := s.scanAllVersions()
	if err != nil {
		return PackageMeta{}, err
	}

	relevantPkgs := s.allPackages[provider].FilterProviderPlatformExactVersion(provider, target, version)
	if len(relevantPkgs) == 0 {
		// This is the local equivalent of a "404 Not Found" when retrieving
		// a particular version from a registry or network mirror. Because
		// the caller should've selected a version already found by
		// AvailableVersions, the only discriminator that should fail here
		// is the target platform, and so our error result assumes that,
		// causing the caller to return an error like "This provider version is
		// not compatible with aros_riscv".
		return PackageMeta{}, ErrPlatformNotSupported{
			Provider: provider,
			Version:  version,
			Platform: target,
		}
	}

	// It's possible that there could be multiple copies of the same package
	// available in the filesystem, if e.g. there's both a packed and an
	// unpacked variant. For now we assume that the decision between them
	// is arbitrary and just take the first one in the result.
	return relevantPkgs[0], nil
}

// AllAvailablePackages scans the directory structure under the source's base
// directory for locally-mirrored packages for all providers, returning a map
// of the discovered packages with the fully-qualified provider names as
// keys.
//
// This is not an operation generally supported by all Source implementations,
// but the filesystem implementation offers it because we also use the
// filesystem mirror source directly to scan our auto-install plugin directory
// and in other automatic discovery situations.
func (s *FilesystemMirrorSource) AllAvailablePackages() (map[addrs.Provider]PackageMetaList, error) {
	// s.allPackages is populated if scanAllVersions succeeds
	err := s.scanAllVersions()
	return s.allPackages, err
}

func (s *FilesystemMirrorSource) scanAllVersions() error {
	s.lock.Lock()
	defer s.lock.Unlock()

	if s.allPackages != nil {
		// we're distinguishing nil-ness from emptiness here so we can
		// recognize when we've scanned the directory without errors, even
		// if we found nothing during the scan.
		return nil
	}

	ret, err := SearchLocalDirectory(s.baseDir)
	if err != nil {
		return err
	}

	// As noted above, we use an explicit empty map so we can distinguish a
	// successful-but-empty result from a failure on future calls, so we'll
	// make sure that's what we have before we assign it here.
	if ret == nil {
		ret = make(map[addrs.Provider]PackageMetaList)
	}
	s.allPackages = ret
	return nil
}

func (s *FilesystemMirrorSource) ForDisplay(provider addrs.Provider) string {
	return s.baseDir
}
