package buildpacks

import (
	"archive/tar"
	"context"
	"errors"
	"fmt"
	"io"
	"runtime"
	"strings"

	"github.com/docker/docker/api/types/container"
	"github.com/docker/docker/client"
)

// Hack implementation of DockerClient that overrides CopyToContainer method.
// The CopyToContainer method hijacks the uploaded project stream and injects function scaffolding to it.
// It basically moves content of /workspace to /workspace/fn and then setup scaffolding code directly in /workspace.
type pyScaffoldInjector struct {
	client.CommonAPIClient
	invoke string
}

func (s pyScaffoldInjector) CopyToContainer(ctx context.Context, ctr, p string, r io.Reader, opts container.CopyToContainerOptions) error {

	if pc, _, _, ok := runtime.Caller(1); ok &&
		!strings.Contains(runtime.FuncForPC(pc).Name(), "build.copyDir") {
		// We are not called by "project dir copy" so we do simple direct forward call.
		return s.CommonAPIClient.CopyToContainer(ctx, ctr, p, r, opts)
	}

	pr, pw := io.Pipe()
	go func() {
		var err error
		defer func() {
			_ = pw.CloseWithError(err)
		}()
		tr := tar.NewReader(r)
		tw := tar.NewWriter(pw)

		for {
			var hdr *tar.Header
			hdr, err = tr.Next()

			if err != nil {
				if errors.Is(err, io.EOF) {
					err = nil
					break
				}
				return
			}

			if strings.HasPrefix(hdr.Name, "/workspace/") {
				hdr.Name = strings.Replace(hdr.Name, "/workspace/", "/workspace/fn/", 1)
			}

			err = tw.WriteHeader(hdr)
			if err != nil {
				return
			}
			_, err = io.Copy(tw, tr)
			if err != nil {
				return
			}
		}
		err = writePythonScaffolding(tw, s.invoke)
		if err != nil {
			return
		}
		err = tw.Close()
	}()

	return s.CommonAPIClient.CopyToContainer(ctx, ctr, p, pr, opts)
}

func writePythonScaffolding(tw *tar.Writer, invoke string) error {
	for _, f := range []struct {
		path    string
		content string
	}{
		{
			path:    "pyproject.toml",
			content: pyprojectToml,
		},
		{
			path:    "service/main.py",
			content: serviceMain(invoke),
		},
		{
			path:    "service/__init__.py",
			content: "",
		},
	} {
		err := tw.WriteHeader(&tar.Header{
			Name: "/workspace/" + f.path,
			Size: int64(len(f.content)),
			Mode: 0644,
		})
		if err != nil {
			return err
		}
		_, err = tw.Write([]byte(f.content))
		if err != nil {
			return err
		}
	}
	return nil
}

const pyprojectToml = `[project]
name = "service"
description = "an autogenerated service which runs a Function"
version = "0.1.0"
requires-python = ">=3.9"
license = "MIT"
dependencies = [
    "func-python",
    "function @ ./fn"
]
authors = [
  { name="The Knative Authors", email="knative-dev@googlegroups.com"},
]

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.metadata]
allow-direct-references = true

[tool.poetry.dependencies]
python = ">=3.9,<4.0"

[tool.poetry.scripts]
script = "service.main:main"
`

func serviceMain(invoke string) string {
	template := `"""
This code is glue between a user's Function and the middleware which will
expose it as a network service.  This code is written on-demand when a
Function is being built, deployed or run.  This will be included in the
final container.
"""
import logging
from func_python.%s import serve

logging.basicConfig(level=logging.INFO)

try:
    from function import new as handler  # type: ignore[import]
except ImportError:
    try:
        from function import handle as handler  # type: ignore[import]
    except ImportError:
        logging.error("Function must export either 'new' or 'handle'")
        raise


def main():
    logging.info("Functions middleware invoking user function")
    serve(handler)

if __name__ == "__main__":
    main()
`
	return fmt.Sprintf(template, invoke)
}
