/*
Copyright IBM Corp. All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package container

import (
	"io"
	"sync"
	"time"

	"github.com/hyperledger/fabric-lib-go/common/flogging"
	"github.com/hyperledger/fabric/core/chaincode/persistence"
	"github.com/hyperledger/fabric/core/container/ccintf"

	"github.com/pkg/errors"
)

var vmLogger = flogging.MustGetLogger("container")

//go:generate counterfeiter -o mock/docker_builder.go --fake-name DockerBuilder . DockerBuilder

// DockerBuilder is what is exposed by the dockercontroller
type DockerBuilder interface {
	Build(ccid string, metadata *persistence.ChaincodePackageMetadata, codePackageStream io.Reader) (Instance, error)
}

//go:generate counterfeiter -o mock/external_builder.go --fake-name ExternalBuilder . ExternalBuilder

// ExternalBuilder is what is exposed by the dockercontroller
type ExternalBuilder interface {
	Build(ccid string, metadata []byte, codePackageStream io.Reader) (Instance, error)
}

//go:generate counterfeiter -o mock/instance.go --fake-name Instance . Instance

// Instance represents a built chaincode instance, because of the docker legacy, calling this a
// built 'container' would be very misleading, and going forward with the external launcher
// 'image' also seemed inappropriate.  So, the vague 'Instance' is used here.
type Instance interface {
	Start(peerConnection *ccintf.PeerConnection) error
	ChaincodeServerInfo() (*ccintf.ChaincodeServerInfo, error)
	Stop() error
	Wait() (int, error)
}

type UninitializedInstance struct{}

func (UninitializedInstance) Start(peerConnection *ccintf.PeerConnection) error {
	return errors.Errorf("instance has not yet been built, cannot be started")
}

func (UninitializedInstance) ChaincodeServerInfo() (*ccintf.ChaincodeServerInfo, error) {
	return nil, errors.Errorf("instance has not yet been built, cannot get chaincode server info")
}

func (UninitializedInstance) Stop() error {
	return errors.Errorf("instance has not yet been built, cannot be stopped")
}

func (UninitializedInstance) Wait() (int, error) {
	return 0, errors.Errorf("instance has not yet been built, cannot wait")
}

//go:generate counterfeiter -o mock/package_provider.go --fake-name PackageProvider . PackageProvider

// PackageProvider gets chaincode packages from the filesystem.
type PackageProvider interface {
	GetChaincodePackage(packageID string) (md *persistence.ChaincodePackageMetadata, mdBytes []byte, codeStream io.ReadCloser, err error)
}

type Router struct {
	ExternalBuilder ExternalBuilder
	DockerBuilder   DockerBuilder
	containers      map[string]Instance
	PackageProvider PackageProvider
	mutex           sync.Mutex
}

func (r *Router) getInstance(ccid string) Instance {
	r.mutex.Lock()
	defer r.mutex.Unlock()

	// Note, to resolve the locking problem which existed in the previous code, we never delete
	// references from the map.  In this way, it is safe to release the lock and operate
	// on the returned reference
	vm, ok := r.containers[ccid]
	if !ok {
		return UninitializedInstance{}
	}

	return vm
}

func (r *Router) Build(ccid string) error {
	var instance Instance

	if r.ExternalBuilder != nil {
		// for now, the package ID we retrieve from the FS is always the ccid
		// the chaincode uses for registration
		_, mdBytes, codeStream, err := r.PackageProvider.GetChaincodePackage(ccid)
		if err != nil {
			return errors.WithMessage(err, "failed to get chaincode package for external build")
		}
		defer codeStream.Close()

		instance, err = r.ExternalBuilder.Build(ccid, mdBytes, codeStream)
		if err != nil {
			return errors.WithMessage(err, "external builder failed")
		}
	}

	if instance == nil {
		if r.DockerBuilder == nil {
			return errors.New("no DockerBuilder, cannot build")
		}
		metadata, _, codeStream, err := r.PackageProvider.GetChaincodePackage(ccid)
		if err != nil {
			return errors.WithMessage(err, "failed to get chaincode package for docker build")
		}
		defer codeStream.Close()

		instance, err = r.DockerBuilder.Build(ccid, metadata, codeStream)
		if err != nil {
			return errors.WithMessage(err, "docker build failed")
		}
	}

	r.mutex.Lock()
	defer r.mutex.Unlock()
	if r.containers == nil {
		r.containers = map[string]Instance{}
	}
	r.containers[ccid] = instance

	return nil
}

func (r *Router) ChaincodeServerInfo(ccid string) (*ccintf.ChaincodeServerInfo, error) {
	return r.getInstance(ccid).ChaincodeServerInfo()
}

func (r *Router) Start(ccid string, peerConnection *ccintf.PeerConnection) error {
	return r.getInstance(ccid).Start(peerConnection)
}

func (r *Router) Stop(ccid string) error {
	return r.getInstance(ccid).Stop()
}

func (r *Router) Wait(ccid string) (int, error) {
	return r.getInstance(ccid).Wait()
}

func (r *Router) Shutdown(timeout time.Duration) {
	var wg sync.WaitGroup
	for ccid := range r.containers {
		wg.Add(1)
		go func(ccid string) {
			defer wg.Done()
			if err := r.Stop(ccid); err != nil {
				vmLogger.Warnw("failed to stop chaincode", "ccid", ccid, "error", err)
			}
		}(ccid)
	}

	done := make(chan struct{})
	go func() {
		wg.Wait()
		close(done)
	}()

	select {
	case <-time.After(timeout):
		vmLogger.Warning("timeout while stopping external chaincodes")
	case <-done:
	}
}
