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

package providercache

import (
	"context"
	"sync"

	"github.com/opentofu/opentofu/internal/addrs"
	"github.com/opentofu/opentofu/internal/getproviders"
)

// InstallerEvents is a collection of function references that can be
// associated with an Installer object in order to be notified about various
// installation lifecycle events during an install operation.
//
// The set of supported events is primarily motivated by allowing ongoing
// progress reports in the UI of the command running provider installation,
// and so this only exposes information interesting to display and does not
// allow the recipient of the events to influence the ongoing process.
//
// Any of the fields may be left as nil to signal that the caller is not
// interested in the associated event. It's better to leave a field set to
// nil than to assign a do-nothing function into it because the installer
// may choose to skip preparing certain temporary data structures if it can see
// that a particular event is not used.
type InstallerEvents struct {
	// The PendingProviders event is called prior to other events to give
	// the recipient prior notice of the full set of distinct provider
	// addresses it can expect to see mentioned in the other events.
	//
	// A recipient driving a UI might, for example, use this to pre-allocate
	// UI space for status reports for all of the providers and then update
	// those positions in-place as other events arrive.
	PendingProviders func(reqs map[addrs.Provider]getproviders.VersionConstraints)

	// ProviderAlreadyInstalled is called for any provider that was included
	// in PendingProviders but requires no further action because a suitable
	// version is already present in the local provider cache directory.
	//
	// This event can also appear after the QueryPackages... series if
	// querying determines that a version already available is the newest
	// available version.
	ProviderAlreadyInstalled func(provider addrs.Provider, selectedVersion getproviders.Version, inProviderCache bool)

	// The BuiltInProvider... family of events describe the outcome for any
	// requested providers that are built in to OpenTofu. Only one of these
	// methods will be called for each such provider, and no other method
	// will be called for them except that they are included in the
	// aggregate PendingProviders map.
	//
	// The "Available" event reports that the requested builtin provider is
	// available in this release of OpenTofu. The "Failure" event reports
	// either that the provider is unavailable or that the request for it
	// is invalid somehow.
	BuiltInProviderAvailable func(provider addrs.Provider)
	BuiltInProviderFailure   func(provider addrs.Provider, err error)

	// The QueryPackages... family of events delimit the operation of querying
	// a provider source for information about available packages matching
	// a particular version constraint, prior to selecting a single version
	// to install.
	//
	// A particular install operation includes only one query per distinct
	// provider, so a caller can use the provider argument as a unique
	// identifier to correlate between successive events.
	//
	// The Begin, Success, and Failure events will each occur only once per
	// distinct provider.
	//
	// The Warning event is unique to the registry source
	QueryPackagesBegin   func(provider addrs.Provider, versionConstraints getproviders.VersionConstraints, locked bool)
	QueryPackagesSuccess func(provider addrs.Provider, selectedVersion getproviders.Version)
	QueryPackagesFailure func(provider addrs.Provider, err error)
	QueryPackagesWarning func(provider addrs.Provider, warn []string)

	// The LinkFromCache... family of events delimit the operation of linking
	// a selected provider package from the system-wide shared cache into the
	// current configuration's local cache.
	//
	// This sequence occurs after the FetchPackage... sequence if the
	// QueryPackages... sequence selects a version that is already in the
	// system-wide cache, and thus we will skip fetching it from the
	// originating provider source and take it from the shared cache instead.
	//
	// Linking should, in most cases, be a much faster operation than
	// fetching. However, it could still potentially be slow in some unusual
	// cases like a particularly large source package on a system where symlinks
	// are impossible, or when either of the cache directories are on a network
	// filesystem accessed over a slow link.
	LinkFromCacheBegin   func(provider addrs.Provider, version getproviders.Version, cacheRoot string)
	LinkFromCacheSuccess func(provider addrs.Provider, version getproviders.Version, localDir string)
	LinkFromCacheFailure func(provider addrs.Provider, version getproviders.Version, err error)

	// The FetchPackage... family of events delimit the operation of retrieving
	// a package from a particular source location.
	//
	// A particular install operation includes only one fetch per distinct
	// provider, so a caller can use the provider argument as a unique
	// identifier to correlate between successive events.
	//
	// For each provider there will be a FetchPackage... sequence which may
	// or may not be followed by a LinkFromCache... sequence, depending on whether
	// the "fetch" is coming from a real source or from an upstream cache.
	//
	// The Query, Begin, Success, and Failure events will each occur only once
	// per distinct provider.
	FetchPackageMeta    func(provider addrs.Provider, version getproviders.Version) // fetching metadata prior to real download
	FetchPackageBegin   func(provider addrs.Provider, version getproviders.Version, location getproviders.PackageLocation, inProviderCache bool)
	FetchPackageSuccess func(provider addrs.Provider, version getproviders.Version, localDir string, authResult *getproviders.PackageAuthenticationResult)
	FetchPackageFailure func(provider addrs.Provider, version getproviders.Version, err error)

	// CacheDirLockContended is called if acquiring a lock on the specified
	// cache directory takes more than a few seconds, suggesting that some
	// other process is already holding a lock.
	CacheDirLockContended func(cacheDir string)

	// The ProvidersLockUpdated event is called whenever the lock file will be
	// updated. It provides the following information:
	//
	//   - `localHashes`: Hashes computed on the local system by analyzing
	//                    files on disk.
	//   - `signedHashes`: Hashes signed by the private key that the origin
	//                     registry claims is the owner of this provider.
	//   - `priorHashes`: Hashes already present in the lock file before we
	//                    made any changes.
	//
	// The final lock file will be updated with a union of all the provided
	// hashes. It not just likely, but expected that there will be duplicates
	// shared between all three collections of hashes i.e. the local hash and
	// remote hashes could already be in the cached hashes.
	//
	// In addition, we place a guarantee that the hash slices will be ordered
	// in the same manner enforced by the lock file within NewProviderLock.
	ProvidersLockUpdated func(provider addrs.Provider, version getproviders.Version, localHashes []getproviders.Hash, signedHashes []getproviders.Hash, priorHashes []getproviders.Hash)

	// The ProvidersAuthenticated event is called after all fetch operations if at
	// least one provider was fetched successfully.
	ProvidersAuthenticated func(authResults map[addrs.Provider]*getproviders.PackageAuthenticationResult)
}

// OnContext produces a context with all of the same behaviors as the given
// context except that it will additionally carry the receiving
// InstallerEvents.
//
// Passing the resulting context to an installer request will cause the
// installer to send event notifications via the callbacks inside.
func (e *InstallerEvents) OnContext(ctx context.Context) context.Context {
	return context.WithValue(ctx, ctxInstallerEvents, e)
}

// Sync wraps the events in a single lock to allow consumers of the
// InstallerEvents concept to not have to worry about race conditions
// as these events may be emitted by multiple go-routines
func (e *InstallerEvents) Sync() *InstallerEvents {
	lock := sync.Mutex{}

	return &InstallerEvents{
		PendingProviders: func(reqs map[addrs.Provider]getproviders.VersionConstraints) {
			lock.Lock()
			defer lock.Unlock()
			if e.PendingProviders != nil {
				e.PendingProviders(reqs)
			}
		},
		ProviderAlreadyInstalled: func(provider addrs.Provider, selectedVersion getproviders.Version, inProviderCache bool) {
			lock.Lock()
			defer lock.Unlock()
			if e.ProviderAlreadyInstalled != nil {
				e.ProviderAlreadyInstalled(provider, selectedVersion, inProviderCache)
			}
		},
		BuiltInProviderAvailable: func(provider addrs.Provider) {
			lock.Lock()
			defer lock.Unlock()
			if e.BuiltInProviderAvailable != nil {
				e.BuiltInProviderAvailable(provider)
			}
		},
		BuiltInProviderFailure: func(provider addrs.Provider, err error) {
			lock.Lock()
			defer lock.Unlock()
			if e.BuiltInProviderFailure != nil {
				e.BuiltInProviderFailure(provider, err)
			}
		},
		QueryPackagesBegin: func(provider addrs.Provider, versionConstraints getproviders.VersionConstraints, locked bool) {
			lock.Lock()
			defer lock.Unlock()
			if e.QueryPackagesBegin != nil {
				e.QueryPackagesBegin(provider, versionConstraints, locked)
			}
		},
		QueryPackagesSuccess: func(provider addrs.Provider, selectedVersion getproviders.Version) {
			lock.Lock()
			defer lock.Unlock()
			if e.QueryPackagesSuccess != nil {
				e.QueryPackagesSuccess(provider, selectedVersion)
			}
		},
		QueryPackagesFailure: func(provider addrs.Provider, err error) {
			lock.Lock()
			defer lock.Unlock()
			if e.QueryPackagesFailure != nil {
				e.QueryPackagesFailure(provider, err)
			}
		},
		QueryPackagesWarning: func(provider addrs.Provider, warn []string) {
			lock.Lock()
			defer lock.Unlock()
			if e.QueryPackagesWarning != nil {
				e.QueryPackagesWarning(provider, warn)
			}
		},
		LinkFromCacheBegin: func(provider addrs.Provider, version getproviders.Version, cacheRoot string) {
			lock.Lock()
			defer lock.Unlock()
			if e.LinkFromCacheBegin != nil {
				e.LinkFromCacheBegin(provider, version, cacheRoot)
			}
		},
		LinkFromCacheSuccess: func(provider addrs.Provider, version getproviders.Version, localDir string) {
			lock.Lock()
			defer lock.Unlock()
			if e.LinkFromCacheSuccess != nil {
				e.LinkFromCacheSuccess(provider, version, localDir)
			}
		},
		LinkFromCacheFailure: func(provider addrs.Provider, version getproviders.Version, err error) {
			lock.Lock()
			defer lock.Unlock()
			if e.LinkFromCacheFailure != nil {
				e.LinkFromCacheFailure(provider, version, err)
			}
		},
		FetchPackageMeta: func(provider addrs.Provider, version getproviders.Version) {
			lock.Lock()
			defer lock.Unlock()
			if e.FetchPackageMeta != nil {
				e.FetchPackageMeta(provider, version)
			}
		},
		FetchPackageBegin: func(provider addrs.Provider, version getproviders.Version, location getproviders.PackageLocation, inProviderCache bool) {
			lock.Lock()
			defer lock.Unlock()
			if e.FetchPackageBegin != nil {
				e.FetchPackageBegin(provider, version, location, inProviderCache)
			}
		},
		FetchPackageSuccess: func(provider addrs.Provider, version getproviders.Version, localDir string, authResult *getproviders.PackageAuthenticationResult) {
			lock.Lock()
			defer lock.Unlock()
			if e.FetchPackageSuccess != nil {
				e.FetchPackageSuccess(provider, version, localDir, authResult)
			}
		},
		FetchPackageFailure: func(provider addrs.Provider, version getproviders.Version, err error) {
			lock.Lock()
			defer lock.Unlock()
			if e.FetchPackageFailure != nil {
				e.FetchPackageFailure(provider, version, err)
			}
		},
		CacheDirLockContended: func(cacheDir string) {
			lock.Lock()
			defer lock.Unlock()
			if e.CacheDirLockContended != nil {
				e.CacheDirLockContended(cacheDir)
			}
		},
		ProvidersLockUpdated: func(provider addrs.Provider, version getproviders.Version, localHashes []getproviders.Hash, signedHashes []getproviders.Hash, priorHashes []getproviders.Hash) {
			lock.Lock()
			defer lock.Unlock()
			if e.ProvidersLockUpdated != nil {
				e.ProvidersLockUpdated(provider, version, localHashes, signedHashes, priorHashes)
			}
		},
		ProvidersAuthenticated: func(authResults map[addrs.Provider]*getproviders.PackageAuthenticationResult) {
			lock.Lock()
			defer lock.Unlock()
			if e.ProvidersAuthenticated != nil {
				e.ProvidersAuthenticated(authResults)
			}
		},
	}
}

// installerEventsForContext looks on the given context for a registered
// InstallerEvents and returns a pointer to it if so.
//
// For caller convenience, if there is no events object attached to the
// given context this function will construct one that has all of its
// fields set to nil and return that, freeing the caller from having to
// do a nil check on the result before dereferencing it.
func installerEventsForContext(ctx context.Context) *InstallerEvents {
	v := ctx.Value(ctxInstallerEvents)
	if v != nil {
		return v.(*InstallerEvents)
	}
	return &InstallerEvents{}
}

type ctxInstallerEventsType int

const ctxInstallerEvents = ctxInstallerEventsType(0)
