package volsnap

import (
	"archive/tar"
	"compress/gzip"
	"encoding/json"
	"fmt"
	"io"
	"io/fs"
	"os"
	"path"
	"path/filepath"
)

// writeArchive serializes the given host-bind volume directories into a
// gzip-compressed tar at dest. Each volume's files live under an integer
// subdirectory (its manifest Index); a manifest.json at the archive root makes
// the archive self-describing. Returns the manifest describing what was
// captured.
//
// Only regular files and directories are archived. Symlinks and special files
// (devices, sockets, fifos) are skipped — this keeps capture safe and avoids
// recording links whose targets would be meaningless or escape the volume on a
// later restore. A torn snapshot is possible if the app writes during capture;
// callers should surface that caveat.
func writeArchive(dest string, refs []VolumeRef) ([]SnapshotVolume, error) {
	// O_EXCL: never clobber an existing file (filenames are unique per call).
	f, err := os.OpenFile(dest, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600)
	if err != nil {
		return nil, fmt.Errorf("create snapshot file: %w", err)
	}
	defer f.Close()

	gz := gzip.NewWriter(f)
	tw := tar.NewWriter(gz)

	manifest := make([]SnapshotVolume, 0, len(refs))
	for i, ref := range refs {
		manifest = append(manifest, SnapshotVolume{Index: i, Target: ref.Target, Scope: ref.Scope, Source: ref.Source})
		if err := addDir(tw, ref.HostPath, fmt.Sprintf("%d", i)); err != nil {
			_ = tw.Close()
			_ = gz.Close()
			_ = f.Close()
			os.Remove(dest)
			return nil, err
		}
	}

	if err := writeManifestEntry(tw, manifest); err != nil {
		_ = tw.Close()
		_ = gz.Close()
		os.Remove(dest)
		return nil, err
	}

	if err := tw.Close(); err != nil {
		_ = gz.Close()
		os.Remove(dest)
		return nil, fmt.Errorf("finalize tar: %w", err)
	}
	if err := gz.Close(); err != nil {
		os.Remove(dest)
		return nil, fmt.Errorf("finalize gzip: %w", err)
	}
	if err := f.Close(); err != nil {
		os.Remove(dest)
		return nil, fmt.Errorf("close snapshot file: %w", err)
	}
	return manifest, nil
}

// addDir walks root and writes its regular files and directories into tw under
// the given archive prefix.
func addDir(tw *tar.Writer, root, prefix string) error {
	return filepath.WalkDir(root, func(p string, d fs.DirEntry, walkErr error) error {
		if walkErr != nil {
			return fmt.Errorf("walk %s: %w", p, walkErr)
		}
		// Skip symlinks and special files; archive only dirs and regular files.
		if d.Type()&fs.ModeSymlink != 0 {
			return nil
		}
		if !d.IsDir() && !d.Type().IsRegular() {
			return nil
		}

		rel, err := filepath.Rel(root, p)
		if err != nil {
			return fmt.Errorf("relativize %s: %w", p, err)
		}
		name := prefix
		if rel != "." {
			name = path.Join(prefix, filepath.ToSlash(rel))
		}

		info, err := d.Info()
		if err != nil {
			return fmt.Errorf("stat %s: %w", p, err)
		}
		hdr, err := tar.FileInfoHeader(info, "")
		if err != nil {
			return fmt.Errorf("tar header %s: %w", p, err)
		}
		hdr.Name = name
		if d.IsDir() {
			hdr.Name += "/"
		}
		if err := tw.WriteHeader(hdr); err != nil {
			return fmt.Errorf("write tar header %s: %w", name, err)
		}
		if d.IsDir() {
			return nil
		}

		src, err := os.Open(p)
		if err != nil {
			return fmt.Errorf("open %s: %w", p, err)
		}
		defer src.Close()
		if _, err := io.Copy(tw, src); err != nil {
			return fmt.Errorf("copy %s: %w", p, err)
		}
		return nil
	})
}

func writeManifestEntry(tw *tar.Writer, manifest []SnapshotVolume) error {
	data, err := json.MarshalIndent(manifest, "", "  ")
	if err != nil {
		return fmt.Errorf("encode manifest: %w", err)
	}
	hdr := &tar.Header{Name: "manifest.json", Mode: 0o600, Size: int64(len(data)), Typeflag: tar.TypeReg}
	if err := tw.WriteHeader(hdr); err != nil {
		return fmt.Errorf("write manifest header: %w", err)
	}
	if _, err := tw.Write(data); err != nil {
		return fmt.Errorf("write manifest: %w", err)
	}
	return nil
}
