package pkg

import (
	"archive/tar"
	"compress/bzip2"
	"compress/gzip"
	"errors"
	"fmt"
	"io"
	"io/fs"
	"net/http"
	"os"
	"path/filepath"
)

const (
	// TarUncompressed denotes an uncompressed tarball.
	TarUncompressed = iota
	// TarGzip denotes a tarball compressed via [gzip].
	TarGzip
	// TarBzip2 denotes a tarball compressed via [bzip2].
	TarBzip2
)

// A tarArtifact is an [Artifact] unpacking a tarball backed by a [FileArtifact].
type tarArtifact struct {
	// Caller-supplied backing tarball.
	f Artifact
	// Compression on top of the tarball.
	compression uint32
}

// 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] suffixed with unpack.
func (a *tarArtifactNamed) String() string { return a.name + "-unpack" }

// NewTar returns a new [Artifact] backed by the supplied [Artifact] and
// compression method. The source [Artifact] must be a [FileArtifact].
func NewTar(a Artifact, compression uint32) Artifact {
	ta := tarArtifact{a, compression}
	if s, ok := a.(fmt.Stringer); ok {
		if name := s.String(); name != "" {
			return &tarArtifactNamed{ta, name}
		}
	}
	return &ta
}

// NewHTTPGetTar is abbreviation for NewHTTPGet passed to NewTar.
func NewHTTPGetTar(
	hc *http.Client,
	url string,
	checksum Checksum,
	compression uint32,
) Artifact {
	return NewTar(NewHTTPGet(hc, url, checksum), compression)
}

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

// Params writes compression encoded in little endian.
func (a *tarArtifact) Params(ctx *IContext) { ctx.WriteUint32(a.compression) }

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

// Dependencies returns a slice containing the backing file.
func (a *tarArtifact) Dependencies() []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 tr io.ReadCloser
	if tr, err = t.Open(a.f); err != nil {
		return
	}

	defer func(f io.ReadCloser) {
		if err == nil {
			err = tr.Close()
		}

		closeErr := f.Close()
		if err == nil {
			err = closeErr
		}
	}(tr)
	br := t.cache.getReader(tr)
	defer t.cache.putReader(br)
	tr = io.NopCloser(br)

	switch a.compression {
	case TarUncompressed:
		break

	case TarGzip:
		if tr, err = gzip.NewReader(tr); err != nil {
			return
		}
		break

	case TarBzip2:
		tr = io.NopCloser(bzip2.NewReader(tr))
		break

	default:
		return os.ErrInvalid
	}

	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
	r := tar.NewReader(tr)
	for header, err = r.Next(); err == nil; header, err = r.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, r); 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
}
