package farm

import (
	"bufio"
	"context"
	"errors"
	"fmt"
	"io"
	"os"
	"sort"
	"strings"
	"sync"

	"github.com/containers/buildah/define"
	"github.com/containers/podman/v5/pkg/domain/entities"
	"github.com/containers/podman/v5/pkg/domain/infra"
	"github.com/hashicorp/go-multierror"
	"github.com/sirupsen/logrus"
	lplatform "go.podman.io/common/libimage/platform"
	"go.podman.io/common/pkg/config"
)

// Farm represents a group of connections to builders.
type Farm struct {
	name        string
	localEngine entities.ImageEngine            // not nil -> use local engine, too
	builders    map[string]entities.ImageEngine // name -> builder
}

// Schedule is a description of where and how we'll do builds.
type Schedule struct {
	platformBuilders map[string]string // target->connection
}

func newFarmWithBuilders(_ context.Context, name string, cons []config.Connection, localEngine entities.ImageEngine, buildLocal bool) (*Farm, error) {
	farm := &Farm{
		builders:    make(map[string]entities.ImageEngine),
		localEngine: localEngine,
		name:        name,
	}
	var (
		builderMutex sync.Mutex
		builderGroup multierror.Group
	)
	// Set up the remote connections to handle the builds
	for _, con := range cons {
		builderGroup.Go(func() error {
			fmt.Printf("Connecting to %q\n", con.Name)
			engine, err := infra.NewImageEngine(&entities.PodmanConfig{
				EngineMode:   entities.TunnelMode,
				URI:          con.URI,
				Identity:     con.Identity,
				MachineMode:  con.IsMachine,
				FarmNodeName: con.Name,
			})
			if err != nil {
				return fmt.Errorf("initializing image engine at %q: %w", con.URI, err)
			}

			defer fmt.Printf("Builder %q ready\n", con.Name)
			builderMutex.Lock()
			defer builderMutex.Unlock()
			farm.builders[con.Name] = engine
			return nil
		})
	}
	// If local=true then use the local machine for builds as well
	if buildLocal {
		builderGroup.Go(func() error {
			fmt.Println("Setting up local builder")
			defer fmt.Println("Local builder ready")
			builderMutex.Lock()
			defer builderMutex.Unlock()
			farm.builders[entities.LocalFarmImageBuilderName] = localEngine
			return nil
		})
	}
	if builderError := builderGroup.Wait(); builderError != nil {
		if err := builderError.ErrorOrNil(); err != nil {
			return nil, err
		}
	}
	if len(farm.builders) > 0 {
		defer fmt.Printf("Farm %q ready\n", farm.name)
		return farm, nil
	}
	return nil, errors.New("no builders configured")
}

func NewFarm(ctx context.Context, name string, localEngine entities.ImageEngine, buildLocal bool) (*Farm, error) {
	// Get the destinations of the connections specified in the farm
	name, destinations, err := getFarmDestinations(name)
	if err != nil {
		return nil, err
	}

	return newFarmWithBuilders(ctx, name, destinations, localEngine, buildLocal)
}

// NativePlatforms returns a list of the set of platforms for which the farm
// can build images natively.
func (f *Farm) NativePlatforms(ctx context.Context) ([]string, error) {
	nativeMap := make(map[string]struct{})
	platforms := []string{}
	var (
		nativeMutex sync.Mutex
		nativeGroup multierror.Group
	)
	for _, engine := range f.builders {
		nativeGroup.Go(func() error {
			logrus.Debugf("getting native platform of %q\n", engine.FarmNodeName(ctx))
			defer logrus.Debugf("got native platform of %q", engine.FarmNodeName(ctx))
			inspect, err := engine.FarmNodeInspect(ctx)
			if err != nil {
				return err
			}
			nativeMutex.Lock()
			defer nativeMutex.Unlock()
			for _, platform := range inspect.NativePlatforms {
				nativeMap[platform] = struct{}{}
			}
			return nil
		})
	}
	merr := nativeGroup.Wait()
	if merr != nil {
		if err := merr.ErrorOrNil(); err != nil {
			return nil, err
		}
	}

	for platform := range nativeMap {
		platforms = append(platforms, platform)
	}
	sort.Strings(platforms)
	return platforms, nil
}

// Schedule takes a list of platforms and returns a list of connections which
// can be used to build for those platforms.  It always prefers native builders
// over emulated builders, but will assign a builder which can use emulation
// for a platform if no suitable native builder is available.
//
// If platforms is an empty list, all available native platforms will be
// scheduled.
//
// TODO: add (Priority,Weight *int) a la RFC 2782 to destinations that we know
// of, and factor those in when assigning builds to nodes in here.
func (f *Farm) Schedule(ctx context.Context, platforms []string) (Schedule, error) {
	var (
		err       error
		infoGroup multierror.Group
		infoMutex sync.Mutex
	)
	// If we weren't given a list of target platforms, generate one.
	if len(platforms) == 0 {
		platforms, err = f.NativePlatforms(ctx)
		if err != nil {
			return Schedule{}, fmt.Errorf("reading list of available native platforms: %w", err)
		}
	}

	platformBuilders := make(map[string]string)
	native := make(map[string]string)
	emulated := make(map[string]string)
	var localPlatform string
	// Make notes of which platforms we can build for natively, and which
	// ones we can build for using emulation.
	for name, engine := range f.builders {
		infoGroup.Go(func() error {
			inspect, err := engine.FarmNodeInspect(ctx)
			if err != nil {
				return err
			}
			infoMutex.Lock()
			defer infoMutex.Unlock()
			for _, n := range inspect.NativePlatforms {
				if _, assigned := native[n]; !assigned {
					native[n] = name
				}
				if name == entities.LocalFarmImageBuilderName {
					localPlatform = n
				}
			}
			for _, e := range inspect.EmulatedPlatforms {
				if _, assigned := emulated[e]; !assigned {
					emulated[e] = name
				}
			}
			return nil
		})
	}
	merr := infoGroup.Wait()
	if merr != nil {
		if err := merr.ErrorOrNil(); err != nil {
			return Schedule{}, err
		}
	}
	// Assign a build to the first node that could build it natively, and
	// if there isn't one, the first one that can build it with the help of
	// emulation, and if there aren't any, error out.
	for _, platform := range platforms {
		if builder, ok := native[platform]; ok {
			platformBuilders[platform] = builder
		} else if builder, ok := emulated[platform]; ok {
			platformBuilders[platform] = builder
		} else {
			return Schedule{}, fmt.Errorf("no builder capable of building for platform %q available", platform)
		}
	}
	// If local is set, prioritize building on local
	if localPlatform != "" {
		platformBuilders[localPlatform] = entities.LocalFarmImageBuilderName
	}
	schedule := Schedule{
		platformBuilders: platformBuilders,
	}
	return schedule, nil
}

// Build runs a build using the specified targetplatform:service map.  If all
// builds succeed, it copies the resulting images from the remote hosts to the
// local service and builds a manifest list with the specified reference name.
func (f *Farm) Build(ctx context.Context, schedule Schedule, options entities.BuildOptions, reference string, _ entities.ImageEngine) error {
	switch options.OutputFormat {
	default:
		return fmt.Errorf("unknown output format %q requested", options.OutputFormat)
	case "", define.OCIv1ImageManifest:
		options.OutputFormat = define.OCIv1ImageManifest
	case define.Dockerv2ImageManifest:
	}

	// Build the list of jobs.
	var jobs sync.Map
	type job struct {
		platform string
		os       string
		arch     string
		variant  string
		builder  entities.ImageEngine
	}
	for platform, builderName := range schedule.platformBuilders { // prepare to build
		builder, ok := f.builders[builderName]
		if !ok {
			return fmt.Errorf("unknown builder %q", builderName)
		}
		var rawOS, rawArch, rawVariant string
		p := strings.Split(platform, "/")
		if len(p) > 0 && p[0] != "" {
			rawOS = p[0]
		}
		if len(p) > 1 {
			rawArch = p[1]
		}
		if len(p) > 2 {
			rawVariant = p[2]
		}
		os, arch, variant := lplatform.Normalize(rawOS, rawArch, rawVariant)
		jobs.Store(builderName, job{
			platform: platform,
			os:       os,
			arch:     arch,
			variant:  variant,
			builder:  builder,
		})
	}

	listBuilderOptions := listBuilderOptions{
		cleanup:       options.Cleanup,
		iidFile:       options.IIDFile,
		authfile:      options.Authfile,
		skipTLSVerify: options.SkipTLSVerify,
	}
	manifestListBuilder := newManifestListBuilder(reference, f.localEngine, listBuilderOptions)

	// Start builds in parallel and wait for them all to finish.
	var (
		buildResults sync.Map
		buildGroup   multierror.Group
	)
	type buildResult struct {
		report  entities.BuildReport
		builder entities.ImageEngine
	}
	for platform, builder := range schedule.platformBuilders {
		outReader, outWriter := io.Pipe()
		errReader, errWriter := io.Pipe()
		go func() {
			defer outReader.Close()
			reader := bufio.NewReader(outReader)
			writer := options.Out
			if writer == nil {
				writer = os.Stdout
			}
			line, err := reader.ReadString('\n')
			for err == nil {
				line = strings.TrimSuffix(line, "\n")
				fmt.Fprintf(writer, "[%s@%s] %s\n", platform, builder, line)
				line, err = reader.ReadString('\n')
			}
		}()
		go func() {
			defer errReader.Close()
			reader := bufio.NewReader(errReader)
			writer := options.Err
			if writer == nil {
				writer = os.Stderr
			}
			line, err := reader.ReadString('\n')
			for err == nil {
				line = strings.TrimSuffix(line, "\n")
				fmt.Fprintf(writer, "[%s@%s] %s\n", platform, builder, line)
				line, err = reader.ReadString('\n')
			}
		}()
		buildGroup.Go(func() error {
			var j job
			defer outWriter.Close()
			defer errWriter.Close()
			c, ok := jobs.Load(builder)
			if !ok {
				return fmt.Errorf("unknown connection for %q (shouldn't happen)", builder)
			}
			if j, ok = c.(job); !ok {
				return fmt.Errorf("unexpected connection type for %q (shouldn't happen)", builder)
			}
			buildOptions := options
			buildOptions.Platforms = []struct{ OS, Arch, Variant string }{{j.os, j.arch, j.variant}}
			buildOptions.Out = outWriter
			buildOptions.Err = errWriter
			fmt.Printf("Starting build for %v at %q\n", buildOptions.Platforms, builder)
			buildReport, err := j.builder.Build(ctx, options.ContainerFiles, buildOptions)
			if err != nil {
				return fmt.Errorf("building for %q on %q: %w", j.platform, builder, err)
			}
			fmt.Printf("finished build for %v at %q: built %s\n", buildOptions.Platforms, builder, buildReport.ID)
			buildResults.Store(platform, buildResult{
				report:  *buildReport,
				builder: j.builder,
			})
			return nil
		})
	}
	buildErrors := buildGroup.Wait()
	if err := buildErrors.ErrorOrNil(); err != nil {
		return fmt.Errorf("building: %w", err)
	}

	// Assemble the final result.
	perArchBuilds := make(map[entities.BuildReport]entities.ImageEngine)
	buildResults.Range(func(_, v any) bool {
		result, ok := v.(buildResult)
		if !ok {
			fmt.Fprintf(os.Stderr, "report %v not a build result?", v)
			return false
		}
		perArchBuilds[result.report] = result.builder
		return true
	})
	location, err := manifestListBuilder.build(ctx, perArchBuilds)
	if err != nil {
		return err
	}
	fmt.Printf("Saved list to %q\n", location)
	return nil
}

func getFarmDestinations(name string) (string, []config.Connection, error) {
	cfg, err := config.Default()
	if err != nil {
		return "", nil, err
	}

	if name == "" {
		if name, cons, err := cfg.GetDefaultFarmConnections(); err == nil {
			// Use default farm if is there is one
			return name, cons, nil
		}
		// If no farm name is given, then grab all the service destinations available
		cons, err := cfg.GetAllConnections()
		return name, cons, err
	}
	cons, err := cfg.GetFarmConnections(name)
	return name, cons, err
}
