package mktarball

import (
	"archive/tar"
	"compress/gzip"
	"fmt"
	"io"
	"log/slog"
	"os"
	"path/filepath"
	"runtime"

	"github.com/Masterminds/semver/v3"
	"github.com/TecharoHQ/yeet/internal"
	"github.com/TecharoHQ/yeet/internal/pkgmeta"
	"github.com/TecharoHQ/yeet/internal/vfs"
)

func defaultFname(p pkgmeta.Package) string {
	return fmt.Sprintf("%s-%s-%s-%s", p.Name, p.Version, p.Platform, p.Goarch)
}

func Build(p pkgmeta.Package) (foutpath string, err error) {
	defer func() {
		if r := recover(); r != nil {
			if err, ok := r.(error); ok {
				slog.Error("mktarball: error while building", "err", err)
			} else {
				err = fmt.Errorf("%v", r)
				slog.Error("mktarball: error while building", "err", err)
			}
		}
	}()

	os.MkdirAll(*internal.PackageDestDir, 0755)
	os.WriteFile(filepath.Join(*internal.PackageDestDir, ".gitignore"), []byte("*\n!.gitignore"), 0644)

	if p.Version == "" {
		p.Version = internal.GitVersion()
	}

	if _, err := semver.NewVersion(p.Version); err != nil {
		return "", fmt.Errorf("invalid version %q: %w", p.Version, err)
	}

	if p.Platform == "" {
		p.Platform = "linux"
	}

	dir, err := os.MkdirTemp("", "yeet-mktarball")
	if err != nil {
		return "", fmt.Errorf("can't make temporary directory")
	}
	defer os.RemoveAll(dir)

	folderName := defaultFname(p)
	if p.Filename != nil {
		folderName = p.Filename(p)
	}

	pkgDir := filepath.Join(dir, folderName)
	os.MkdirAll(pkgDir, 0755)

	fname := filepath.Join(*internal.PackageDestDir, folderName+".tar.gz")
	fout, err := os.Create(fname)
	if err != nil {
		return "", fmt.Errorf("can't make output file: %w", err)
	}
	defer fout.Close()

	gw, err := gzip.NewWriterLevel(fout, 9)
	if err != nil {
		return "", fmt.Errorf("can't make gzip writer: %w", err)
	}
	defer gw.Close()

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

	cgoEnabled := os.Getenv("CGO_ENABLED")
	defer func() {
		os.Setenv("GOARCH", runtime.GOARCH)
		os.Setenv("GOOS", runtime.GOOS)
		os.Setenv("CGO_ENABLED", cgoEnabled)
	}()
	os.Setenv("GOARCH", p.Goarch)
	os.Setenv("GOOS", p.Platform)
	os.Setenv("CGO_ENABLED", "0")

	bi := pkgmeta.BuildInput{
		Output:  pkgDir,
		Bin:     filepath.Join(pkgDir, "bin"),
		Doc:     filepath.Join(pkgDir, "doc"),
		Etc:     filepath.Join(pkgDir, "run"),
		Man:     filepath.Join(pkgDir, "man"),
		Systemd: filepath.Join(pkgDir, "run"),
	}

	os.MkdirAll(bi.Doc, 0755)
	os.WriteFile(filepath.Join(bi.Doc, "VERSION"), []byte(p.Version+"\n"), 0666)

	p.Build(bi)

	for src, dst := range p.ConfigFiles {
		if err := Copy(src, filepath.Join(bi.Etc, dst)); err != nil {
			return "", fmt.Errorf("can't copy %s to %s: %w", src, dst, err)
		}
	}

	for src, dst := range p.Documentation {
		fname := filepath.Join(bi.Doc, dst)
		if filepath.Base(fname) == "README.md" {
			fname = filepath.Join(pkgDir, "README.md")
		}

		if err := Copy(src, fname); err != nil {
			return "", fmt.Errorf("can't copy %s to %s: %w", src, dst, err)
		}
	}

	root, err := os.OpenRoot(dir)
	if err != nil {
		return "", fmt.Errorf("can't open root FS %s: %w", dir, err)
	}

	if err := tw.AddFS(vfs.ModTimeFS{FS: root.FS(), Time: internal.SourceEpoch()}); err != nil {
		return "", fmt.Errorf("can't copy built files to tarball: %w", err)
	}

	slog.Info("built package", "name", p.Name, "arch", p.Goarch, "version", p.Version, "path", fout.Name())

	return fname, nil
}

// Copy copies the contents of the file at srcpath to a regular file
// at dstpath. If the file named by dstpath already exists, it is
// truncated. The function does not copy the file mode, file
// permission bits, or file attributes.
func Copy(srcpath, dstpath string) (err error) {
	r, err := os.Open(srcpath)
	if err != nil {
		return err
	}
	defer r.Close() // ignore error: file was opened read-only.

	st, err := r.Stat()
	if err != nil {
		return err
	}

	os.MkdirAll(filepath.Dir(dstpath), 0755)

	w, err := os.Create(dstpath)
	if err != nil {
		return err
	}

	if err := w.Chmod(st.Mode()); err != nil {
		return err
	}

	defer func() {
		// Report the error, if any, from Close, but do so
		// only if there isn't already an outgoing error.
		if c := w.Close(); err == nil {
			err = c
		}
	}()

	_, err = io.Copy(w, r)
	return err
}
