package compression

import (
	"io"
	"os"
	"path/filepath"
	"strings"

	"github.com/containers/podman/v5/pkg/machine/define"
	"github.com/containers/podman/v5/utils"
	"github.com/containers/storage/pkg/archive"
	"github.com/sirupsen/logrus"
)

const (
	decompressedFileFlag = os.O_CREATE | os.O_TRUNC | os.O_WRONLY
	macOs                = "darwin"
	progressBarPrefix    = "Extracting compressed file"
	zipExt               = ".zip"
	magicNumberMaxBytes  = 10
)

type decompressor interface {
	compressedFileSize() int64
	compressedFileMode() os.FileMode
	compressedFileReader() (io.ReadCloser, error)
	decompress(w io.WriteSeeker, r io.Reader) error
	close()
}

func Decompress(compressedVMFile *define.VMFile, decompressedFilePath string) error {
	compressedFilePath := compressedVMFile.GetPath()
	compressedFileMagicNum, err := compressedVMFile.ReadMagicNumber(magicNumberMaxBytes)
	if err != nil {
		return err
	}

	var d decompressor
	if d, err = newDecompressor(compressedFilePath, compressedFileMagicNum); err != nil {
		return err
	}

	return runDecompression(d, decompressedFilePath)
}

func newDecompressor(compressedFilePath string, compressedFileMagicNum []byte) (decompressor, error) {
	compressionType := archive.DetectCompression(compressedFileMagicNum)
	hasZipSuffix := strings.HasSuffix(compressedFilePath, zipExt)

	switch {
	// Zip files are not guaranteed to have a magic number at the beginning
	// of the file, so we need to use the file name to detect them.
	case compressionType == archive.Uncompressed && hasZipSuffix:
		return newZipDecompressor(compressedFilePath)
	case compressionType == archive.Uncompressed:
		return newUncompressedDecompressor(compressedFilePath)
	default:
		return newGenericDecompressor(compressedFilePath)
	}
}

func runDecompression(d decompressor, decompressedFilePath string) (retErr error) {
	compressedFileReader, err := d.compressedFileReader()
	if err != nil {
		return err
	}
	defer d.close()

	filesize := d.compressedFileSize()
	// bar.ProxyReader() returns nil when the bar is finished.
	// When the size is set to 0 the bar will finfish immediately, but this happens
	// in a different goroutine so it is race and only happens sometimes.
	// In general if the input is an empty file then we do not have to display a
	// progress bar at all as there is nothing to copy/extract really
	// https://github.com/containers/podman/issues/23281
	if filesize > 0 {
		initMsg := progressBarPrefix + ": " + filepath.Base(decompressedFilePath)
		finalMsg := initMsg + ": done"

		p, bar := utils.ProgressBar(initMsg, filesize, finalMsg)
		// Wait for bars to complete and then shut down the bars container
		defer p.Wait()

		compressedFileReader = bar.ProxyReader(compressedFileReader)
		// Interrupts the bar goroutine. It's important that
		// bar.Abort(false) is called before p.Wait(), otherwise
		// can hang.
		defer bar.Abort(false)
	}

	var decompressedFileWriter *os.File

	if decompressedFileWriter, err = os.OpenFile(decompressedFilePath, decompressedFileFlag, d.compressedFileMode()); err != nil {
		logrus.Errorf("Unable to open destination file %s for writing: %q", decompressedFilePath, err)
		return err
	}
	defer func() {
		if err := decompressedFileWriter.Close(); err != nil {
			logrus.Warnf("Unable to to close destination file %s: %q", decompressedFilePath, err)
			if retErr == nil {
				retErr = err
			}
		}
	}()

	if err = d.decompress(decompressedFileWriter, compressedFileReader); err != nil {
		logrus.Errorf("Error extracting compressed file: %q", err)
		return err
	}

	return nil
}
