package getter

import (
	"archive/tar"
	"compress/gzip"
	"context"
	"io"
	"net/url"
	"os"
	"path/filepath"
	"time"

	"github.com/opencontainers/go-digest"
	"github.com/pkg/errors"

	"hauler.dev/go/hauler/pkg/artifacts"
	"hauler.dev/go/hauler/pkg/consts"
)

type directory struct {
	*File
}

func NewDirectory() *directory {
	return &directory{File: NewFile()}
}

func (d directory) Open(ctx context.Context, u *url.URL) (io.ReadCloser, error) {
	tmpfile, err := os.CreateTemp("", "hauler")
	if err != nil {
		return nil, err
	}

	digester := digest.Canonical.Digester()
	zw := gzip.NewWriter(io.MultiWriter(tmpfile, digester.Hash()))
	defer zw.Close()

	tarDigester := digest.Canonical.Digester()
	if err := tarDir(d.path(u), d.Name(u), io.MultiWriter(zw, tarDigester.Hash()), false); err != nil {
		return nil, err
	}

	if err := zw.Close(); err != nil {
		return nil, err
	}
	if err := tmpfile.Sync(); err != nil {
		return nil, err
	}

	fi, err := os.Open(tmpfile.Name())
	if err != nil {
		return nil, err
	}

	// rc := &closer{
	// 	t: io.TeeReader(tmpfile, fi),
	// 	closes: []func() error{fi.Close, tmpfile.Close, zw.Close},
	// }
	return fi, nil
}

func (d directory) Detect(u *url.URL) bool {
	if len(d.path(u)) == 0 {
		return false
	}

	fi, err := os.Stat(d.path(u))
	if err != nil {
		return false
	}
	return fi.IsDir()
}

func (d directory) Config(u *url.URL) artifacts.Config {
	c := &directoryConfig{
		config{Reference: u.String()},
	}
	return artifacts.ToConfig(c, artifacts.WithConfigMediaType(consts.FileDirectoryConfigMediaType))
}

type directoryConfig struct {
	config `json:",inline,omitempty"`
}

func tarDir(root string, prefix string, w io.Writer, stripTimes bool) error {
	tw := tar.NewWriter(w)
	defer tw.Close()
	if err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}

		// Rename path
		name, err := filepath.Rel(root, path)
		if err != nil {
			return err
		}
		name = filepath.Join(prefix, name)
		name = filepath.ToSlash(name)

		// Generate header
		var link string
		mode := info.Mode()
		if mode&os.ModeSymlink != 0 {
			if link, err = os.Readlink(path); err != nil {
				return err
			}
		}
		header, err := tar.FileInfoHeader(info, link)
		if err != nil {
			return errors.Wrap(err, path)
		}
		header.Name = name
		header.Uid = 0
		header.Gid = 0
		header.Uname = ""
		header.Gname = ""

		if stripTimes {
			header.ModTime = time.Time{}
			header.AccessTime = time.Time{}
			header.ChangeTime = time.Time{}
		}

		// Write file
		if err := tw.WriteHeader(header); err != nil {
			return errors.Wrap(err, "tar")
		}
		if mode.IsRegular() {
			file, err := os.Open(path)
			if err != nil {
				return err
			}
			defer file.Close()
			if _, err := io.Copy(tw, file); err != nil {
				return errors.Wrap(err, path)
			}
		}

		return nil
	}); err != nil {
		return err
	}
	return nil
}

type closer struct {
	t      io.Reader
	closes []func() error
}

func (c *closer) Read(p []byte) (n int, err error) {
	return c.t.Read(p)
}

func (c *closer) Close() error {
	var err error
	for _, c := range c.closes {
		lastErr := c()
		if err == nil {
			err = lastErr
		}
	}
	return err
}
