package pkg

import (
	"archive/tar"
	"errors"
	"fmt"
	"io"
	"io/fs"
	"os"
	"path/filepath"
)

// A tarArtifact is an [Artifact] unpacking a tarball backed by a [FileArtifact].
type tarArtifact struct {
	// Caller-supplied backing tarball.
	f Artifact
}

var _ CuresExempt = new(tarArtifact)

// tarArtifactNamed embeds tarArtifact for a [fmt.Stringer] tarball.
type tarArtifactNamed struct {
	tarArtifact
	// Copied from tarArtifact.f.
	name string
}

var _ fmt.Stringer = new(tarArtifactNamed)

// String returns the name of the underlying [Artifact] prefixed with unpack.
func (a *tarArtifactNamed) String() string { return "unpack-" + a.name }

// NewTar returns a new [Artifact] unpacking the tar stream produced by the
// backing [Artifact]. The source [Artifact] must be a [FileArtifact].
func NewTar(a Artifact) Artifact {
	ta := tarArtifact{a}
	if s, ok := a.(fmt.Stringer); ok {
		if name := s.String(); name != "" {
			return &tarArtifactNamed{ta, name}
		}
	}
	return &ta
}

// Kind returns the hardcoded [Kind] constant.
func (a *tarArtifact) Kind() Kind { return KindTar }

// Params is a noop.
func (a *tarArtifact) Params(*IContext) {}

func init() {
	register(KindTar, func(r *IRReader) Artifact {
		a := NewTar(r.Next())
		if _, ok := r.Finalise(); ok {
			panic(ErrUnexpectedChecksum)
		}
		return a
	})
}

// Inputs returns a slice containing the backing file.
func (a *tarArtifact) Inputs() []Artifact {
	return []Artifact{a.f}
}

// IsExclusive returns false: decompressor and tar reader are fully sequential.
func (a *tarArtifact) IsExclusive() bool { return false }

// A DisallowedTypeflagError describes a disallowed typeflag encountered while
// unpacking a tarball.
type DisallowedTypeflagError byte

func (e DisallowedTypeflagError) Error() string {
	return "disallowed typeflag '" + string(e) + "'"
}

// Cure cures the [Artifact], producing a directory located at work.
func (a *tarArtifact) Cure(t *TContext) (err error) {
	var r io.ReadCloser
	if r, err = t.Open(a.f); err != nil {
		return
	}

	defer func() {
		closeErr := r.Close()
		if err == nil {
			err = closeErr
		}
	}()

	type dirTargetPerm struct {
		path string
		mode fs.FileMode
	}
	var madeDirectories []dirTargetPerm

	if err = os.MkdirAll(t.GetTempDir().String(), 0700); err != nil {
		return
	}
	var root *os.Root
	if root, err = os.OpenRoot(t.GetTempDir().String()); err != nil {
		return
	}
	defer func() {
		closeErr := root.Close()
		if err == nil {
			err = closeErr
		}
	}()

	var header *tar.Header
	tr := tar.NewReader(r)
	for header, err = tr.Next(); err == nil; header, err = tr.Next() {
		typeflag := header.Typeflag
		if typeflag == 0 {
			if len(header.Name) > 0 && header.Name[len(header.Name)-1] == '/' {
				typeflag = tar.TypeDir
			} else {
				typeflag = tar.TypeReg
			}
		}

		if typeflag >= '0' && typeflag <= '9' && typeflag != tar.TypeDir {
			if err = root.MkdirAll(filepath.Dir(header.Name), 0700); err != nil {
				return
			}
		}

		switch typeflag {
		case tar.TypeReg:
			var f *os.File
			if f, err = root.OpenFile(
				header.Name,
				os.O_CREATE|os.O_EXCL|os.O_WRONLY,
				header.FileInfo().Mode()&0500,
			); err != nil {
				return
			}
			if _, err = io.Copy(f, tr); err != nil {
				_ = f.Close()
				return
			} else if err = f.Close(); err != nil {
				return
			}
			break

		case tar.TypeLink:
			if err = root.Link(
				header.Linkname,
				header.Name,
			); err != nil {
				return
			}
			break

		case tar.TypeSymlink:
			if err = root.Symlink(
				header.Linkname,
				header.Name,
			); err != nil {
				return
			}
			break

		case tar.TypeDir:
			madeDirectories = append(madeDirectories, dirTargetPerm{
				path: header.Name,
				mode: header.FileInfo().Mode(),
			})
			if err = root.MkdirAll(header.Name, 0700); err != nil {
				return
			}
			break

		case tar.TypeXGlobalHeader:
			continue // ignore

		default:
			return DisallowedTypeflagError(typeflag)
		}
	}
	if errors.Is(err, io.EOF) {
		err = nil
	}
	if err == nil {
		for _, e := range madeDirectories {
			if err = root.Chmod(e.path, e.mode&0500); err != nil {
				return
			}
		}
	} else {
		return
	}

	temp := t.GetTempDir()
	if err = os.Chmod(temp.String(), 0700); err != nil {
		return
	}

	var entries []os.DirEntry
	if entries, err = os.ReadDir(temp.String()); err != nil {
		return
	}

	if len(entries) == 1 && entries[0].IsDir() {
		p := temp.Append(entries[0].Name())
		if err = os.Chmod(p.String(), 0700); err != nil {
			return
		}
		err = os.Rename(p.String(), t.GetWorkDir().String())
	} else {
		err = os.Rename(temp.String(), t.GetWorkDir().String())
	}
	return
}

// CuresExempt exempts the cheap [KindTar] implementation often at the end of a
// [FileArtifact] pipeline.
func (*tarArtifact) CuresExempt() {}
