package oci

import (
	"archive/tar"
	"compress/gzip"
	"fmt"
	"io"
	"io/fs"
	"os"
	"os/exec"
	slashpath "path"
	"path/filepath"
	"strings"

	v1 "github.com/google/go-containerregistry/pkg/v1"
	"github.com/google/go-containerregistry/pkg/v1/tarball"
)

type goBuilder struct{}

func (b goBuilder) Base(customImage string) string {
	// if not defined -> return "", meaning building from scratch
	return customImage
}

func (b goBuilder) Configure(_ buildJob, _ v1.Platform, cf v1.ConfigFile) (v1.ConfigFile, error) {
	// : Using Cmd rather than Entrypoint due to it being overrideable.
	cf.Config.Cmd = []string{"/func/f"}
	cf.Config.Env = append(cf.Config.Env, "LISTEN_ADDRESS=[::]:8080")
	return cf, nil
}

func (b goBuilder) WriteShared(_ buildJob) ([]imageLayer, error) {
	return []imageLayer{}, nil // no shared dependencies generated on build
}

// ForPlatform returns layers from source code as Go, cross compiled for the given
// platform, placing the statically linked binary in a tarred layer and return
// the Descriptor and Layer metadata.
func (b goBuilder) WritePlatform(cfg buildJob, p v1.Platform) (layers []imageLayer, err error) {
	var desc v1.Descriptor
	var layer v1.Layer

	// Executable
	exe, err := goBuild(cfg, p) // Compile binary returning its path
	if err != nil {
		return
	}

	// Tarball
	target := filepath.Join(cfg.buildDir(), fmt.Sprintf("execlayer.%v.%v.tar.gz", p.OS, p.Architecture))
	if err = goExeTarball(exe, target, cfg.verbose); err != nil {
		return
	}

	// Layer
	if layer, err = tarball.LayerFromFile(target); err != nil {
		return
	}

	// Descriptor
	if desc, err = newDescriptor(layer); err != nil {
		return
	}
	desc.Platform = &p

	// Blob
	blob := filepath.Join(cfg.blobsDir(), desc.Digest.Hex)
	if cfg.verbose {
		fmt.Printf("mv %v %v\n", rel(cfg.buildDir(), target), rel(cfg.buildDir(), blob))
	}
	err = os.Rename(target, blob)
	if err != nil {
		return nil, fmt.Errorf("cannot rename blob: %w", err)
	}

	// NOTE: base is intentionally blank indiciating it is to be built without
	// a base layer.
	return []imageLayer{{Descriptor: desc, Layer: layer}}, nil
}

func goBuild(cfg buildJob, p v1.Platform) (binPath string, err error) {
	gobin, args, outpath, err := goBuildCmd(p, cfg)
	if err != nil {
		return
	}
	envs := goBuildEnvs(p)
	if cfg.verbose {
		fmt.Printf("%v %v\n", gobin, strings.Join(args, " "))
	} else {
		fmt.Printf("   %v\n", filepath.Base(outpath))
	}

	cmd := exec.CommandContext(cfg.ctx, gobin, "mod", "tidy")
	cmd.Env = envs
	cmd.Dir = cfg.buildDir()
	cmd.Stderr = os.Stderr
	cmd.Stdout = os.Stdout
	err = cmd.Run()
	if err != nil {
		return "", fmt.Errorf("cannot sync deps: %w", err)
	}

	// Build the function
	cmd = exec.CommandContext(cfg.ctx, gobin, args...)
	cmd.Env = envs
	cmd.Dir = cfg.buildDir()
	cmd.Stderr = os.Stderr
	cmd.Stdout = os.Stdout

	return outpath, cmd.Run()
}

func goBuildCmd(p v1.Platform, cfg buildJob) (gobin string, args []string, outpath string, err error) {
	/* TODO:  Use Build Command override from the function if provided
	 * A future PR will include the ability to specify a
	 * f.Build.BuildCommand, or BuildArgs for use here to customize
	 * This will be useful when, for example, the function is written in
	 *	Go and the function developer needs Libc compatibility, in which case
	 *	the default command will need to be replaced with:
	 *	go build -ldflags "-linkmode 'external' -extldflags '-static'"
	 *  Pseudocode:
	 *  if BuildArgs or BuildCommand
	 *    Validate command or args are safe to run
	 *      no other commands injected
	 *      does not contain Go's "toolexec"
	 *      does not specify the output path
	 *    Either replace or append to gobin
	 */

	// Use the binary specified FUNC_GO if defined
	gobin = os.Getenv("FUNC_GO") // TODO: move to main and plumb through
	if gobin == "" {
		gobin = "go"
	}

	// Build as ./func/builds/$PID/result/f.$OS.$Architecture
	name := fmt.Sprintf("f.%v.%v", p.OS, p.Architecture)
	if p.Variant != "" {
		name = name + "." + p.Variant
	}
	outpath = filepath.Join(cfg.buildDir(), "result", name)
	args = []string{"build", "-o", outpath}
	return gobin, args, outpath, nil
}

func goBuildEnvs(p v1.Platform) (envs []string) {
	pegged := []string{
		"CGO_ENABLED=0",
		"GOOS=" + p.OS,
		"GOARCH=" + p.Architecture,
	}
	if p.Variant != "" && p.Architecture == "arm" {
		pegged = append(pegged, "GOARM="+strings.TrimPrefix(p.Variant, "v"))
	} else if p.Variant != "" && p.Architecture == "amd64" {
		pegged = append(pegged, "GOAMD64="+p.Variant)
	}

	isPegged := func(env string) bool {
		for _, v := range pegged {
			name := strings.Split(v, "=")[0]
			if strings.HasPrefix(env, name) {
				return true
			}
		}
		return false
	}

	envs = append(envs, pegged...)
	for _, env := range os.Environ() {
		if !isPegged(env) {
			envs = append(envs, env)
		}
	}
	return envs
}

func goExeTarball(source, target string, verbose bool) error {
	targetFile, err := os.Create(target)
	if err != nil {
		return err
	}
	defer targetFile.Close()

	gw := gzip.NewWriter(targetFile)
	defer gw.Close()

	tw := tar.NewWriter(gw)
	defer tw.Close()

	info, err := os.Stat(source)
	if err != nil {
		return err
	}

	header, err := tar.FileInfoHeader(info, info.Name())
	if err != nil {
		return err
	}
	header.Mode = (header.Mode & ^int64(fs.ModePerm)) | 0755

	header.Name = slashpath.Join("/func", "f")
	// TODO: should we set file timestamps to the build start time of cfg.t?
	// header.ModTime = timestampArgument

	if err = tw.WriteHeader(header); err != nil {
		return err
	}
	if verbose {
		fmt.Printf("→ %v \n", header.Name)
	}

	file, err := os.Open(source)
	if err != nil {
		return err
	}
	defer file.Close()

	i, err := io.Copy(tw, file)
	if err != nil {
		return err
	}
	if verbose {
		fmt.Printf("  wrote %v bytes \n", i)
	}
	return nil
}
