package oci

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

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

var defaultPythonBase = "python:3.13-slim" // Moving from docker.io.  See issue #2720

type pythonBuilder struct{}

func (b pythonBuilder) Base(customBase string) string {
	if customBase != "" {
		return customBase
	}
	cmd := exec.Command("python", "-V")
	out, err := cmd.CombinedOutput()
	if err != nil {
		return defaultPythonBase
	}
	re := regexp.MustCompile(`Python (\d+\.\d+)\.\d+`)
	subMatches := re.FindSubmatch(out)
	if len(subMatches) != 2 {
		return defaultPythonBase
	}
	return fmt.Sprintf("python:%s-slim", subMatches[1])
}

// Configure gives the python builder a chance to mutate the final
// ConfigFile that will be used when building the template.
func (b pythonBuilder) Configure(job buildJob, _ v1.Platform, cf v1.ConfigFile) (v1.ConfigFile, error) {
	var (
		svcRelPath, _ = filepath.Rel(job.function.Root, job.buildDir()) // eg .func/builds/by-hash/$HASH
		svcPath       = filepath.Join("/func", svcRelPath)              // eg /func/.func/builds/by-hash/$HASH
		pythonPathEnv = fmt.Sprintf("PYTHONPATH=%v/lib", svcPath)
		mainPath      = fmt.Sprintf("%v/service/main.py", svcPath)
		listenAddrEnv = "LISTEN_ADDRESS=[::]:8080"
	)

	cf.Config.Env = append(cf.Config.Env, pythonPathEnv, listenAddrEnv)
	cf.Config.Cmd = []string{"python", mainPath}
	return cf, nil
}

func (b pythonBuilder) WriteShared(job buildJob) (layers []imageLayer, err error) {
	var desc v1.Descriptor
	var layer v1.Layer

	// Create venv
	if job.verbose {
		fmt.Printf("python -m venv .venv\n")
	}
	cmd := exec.CommandContext(job.ctx, "python", "-m", "venv", ".venv")
	cmd.Dir = job.buildDir()
	cmd.Stderr = os.Stderr
	cmd.Stdout = os.Stdout
	if err = cmd.Run(); err != nil {
		return
	}

	pipPath := filepath.Join(".venv", "bin", "pip")

	// Upgrade pip
	if job.verbose {
		fmt.Printf(".venv/bin/pip install --upgrade pip\n")
	}
	cmd = exec.CommandContext(job.ctx, pipPath, "install", "--upgrade", "pip")
	cmd.Dir = job.buildDir()
	cmd.Stderr = os.Stderr
	cmd.Stdout = os.Stdout
	if err = cmd.Run(); err != nil {
		return
	}

	// Install Dependencies of the current project into ./lib
	// In the scaffolding direcotory.
	if job.verbose {
		fmt.Printf(".venv/bin/pip install . --target lib\n")
	}
	cmd = exec.CommandContext(job.ctx, pipPath, "install", ".", "--target", "lib")
	cmd.Dir = job.buildDir()
	cmd.Stderr = os.Stderr
	cmd.Stdout = os.Stdout
	if err = cmd.Run(); err != nil {
		return
	}

	// Tar up the now-final build directory
	source := job.buildDir()
	target := filepath.Join(job.buildDir(), "lib.tar.gz")
	if err = newPythonLibTarball(job, source, target); err != nil {
		return
	}

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

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

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

	return []imageLayer{{Descriptor: desc, Layer: layer}}, nil
}

func newPythonLibTarball(job buildJob, root, target string) error {
	// Create a tarball of the "build directory"
	// when extracted, it's root will be /func
	// all files within should have path prefix .func/builds/by-hash/$hash

	targetFile, err := os.Create(target) // final .tar.gz
	if err != nil {
		return err
	}
	defer targetFile.Close()

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

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

	return filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}

		// TODO:
		// This is not ideal because we have to explicitly ignore the
		// oci and venv directories from being tarred and the tar from itself.
		// In hindsight, it would have been better to have the "build"
		// directory contain two sub-directories:
		// ./dist  -  the scaffolding, libraries and link to the source code.
		// ./container  -  the final OCI container.
		if path == job.ociDir() {
			return filepath.SkipDir
		}
		if path == filepath.Join(root, ".venv") {
			return filepath.SkipDir
		}
		if path == target {
			return nil
		}

		lnk := "" // if link, this will be used as the target
		if info.Mode()&fs.ModeSymlink != 0 {
			if lnk, err = validatedLinkTarget(job.function.Root, path); err != nil {
				return err
			}
		}

		header, err := tar.FileInfoHeader(info, lnk)
		if err != nil {
			return err
		}

		// The relative path from the function's root to the file
		relPath, err := filepath.Rel(job.function.Root, path)
		if err != nil {
			return err
		}
		header.Name = slashpath.Join("/func/", filepath.ToSlash(relPath))
		header.Uid = DefaultUid
		header.Gid = DefaultGid
		if err := tw.WriteHeader(header); err != nil {
			return err
		}
		if !info.Mode().IsRegular() { //nothing more to do for non-regular
			return nil
		}

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

		_, err = io.Copy(tw, file)
		return err
	})
}

func (b pythonBuilder) WritePlatform(ctx buildJob, p v1.Platform) (layers []imageLayer, err error) {
	return []imageLayer{}, nil
}
