package deb

import (
	"archive/tar"
	"bufio"
	"compress/bzip2"
	"compress/gzip"
	"fmt"
	"io"
	"os"
	"strings"

	"github.com/h2non/filetype/matchers"
	ar "github.com/mkrautz/goar"
	"github.com/pkg/errors"

	"github.com/aptly-dev/aptly/pgp"
	"github.com/kjk/lzma"
	"github.com/klauspost/compress/zstd"
	xz "github.com/smira/go-xz"
)

// Source kinds
const (
	SourceSnapshot   = "snapshot"
	SourceLocalRepo  = "local"
	SourceRemoteRepo = "repo"
)

type parseQuery func(string) (PackageQuery, error)

// GetControlFileFromDeb reads control file from deb package
func GetControlFileFromDeb(packageFile string) (Stanza, error) {
	file, err := os.Open(packageFile)
	if err != nil {
		return nil, err
	}
	defer func() { _ = file.Close() }()

	library := ar.NewReader(file)
	for {
		header, err := library.Next()

		if err == io.EOF {
			return nil, fmt.Errorf("unable to find control.tar.* part in package %s", packageFile)
		}
		if err != nil {
			return nil, fmt.Errorf("unable to read .deb archive %s: %s", packageFile, err)
		}

		// As per deb(5) version 1.19.0.4 the control file may be:
		// - control.tar (since 1.17.6)
		// - control.tar.gz
		// - control.tar.xz (since 1.17.6)
		// Look for all of the above and uncompress as necessary.
		if strings.HasPrefix(header.Name, "control.tar") {
			bufReader := bufio.NewReader(library)

			var tarInput io.Reader

			switch header.Name {
			case "control.tar":
				tarInput = bufReader
			case "control.tar.gz":
				ungzip, err := gzip.NewReader(bufReader)
				if err != nil {
					return nil, errors.Wrapf(err, "unable to ungzip %s from %s", header.Name, packageFile)
				}
				defer func() { _ = ungzip.Close() }()
				tarInput = ungzip
			case "control.tar.xz":
				unxz, err := xz.NewReader(bufReader)
				if err != nil {
					return nil, errors.Wrapf(err, "unable to unxz %s from %s", header.Name, packageFile)
				}
				defer func() { _ = unxz.Close() }()
				tarInput = unxz
			case "control.tar.zst":
				unzstd, err := zstd.NewReader(bufReader)
				if err != nil {
					return nil, errors.Wrapf(err, "unable to unzstd %s from %s", header.Name, packageFile)
				}
				defer unzstd.Close()
				tarInput = unzstd
			default:
				return nil, fmt.Errorf("unsupported tar compression in %s: %s", packageFile, header.Name)
			}

			untar := tar.NewReader(tarInput)
			for {
				tarHeader, err := untar.Next()
				if err == io.EOF {
					return nil, fmt.Errorf("unable to find control file in %s", packageFile)
				}
				if err != nil {
					return nil, fmt.Errorf("unable to read .tar archive from %s. Error: %s", packageFile, err)
				}

				if tarHeader.Name == "./control" || tarHeader.Name == "control" {
					reader := NewControlFileReader(untar, false, false)
					stanza, err := reader.ReadStanza()
					if err != nil {
						return nil, err
					}

					return stanza, nil
				}
			}
		}
	}
}

// GetControlFileFromDsc reads control file from dsc package
func GetControlFileFromDsc(dscFile string, verifier pgp.Verifier) (Stanza, error) {
	file, err := os.Open(dscFile)
	if err != nil {
		return nil, err
	}
	defer func() { _ = file.Close() }()

	isClearSigned, err := verifier.IsClearSigned(file)
	_, _ = file.Seek(0, 0)

	if err != nil {
		return nil, err
	}

	var text io.ReadCloser

	if isClearSigned {
		text, err = verifier.ExtractClearsigned(file)
		if err != nil {
			return nil, err
		}
		defer func() { _ = text.Close() }()
	} else {
		text = file
	}

	reader := NewControlFileReader(text, false, false)
	stanza, err := reader.ReadStanza()
	if err != nil {
		return nil, err
	}

	return stanza, nil

}

// GetContentsFromDeb returns list of files installed by .deb package
func GetContentsFromDeb(file io.Reader, packageFile string) ([]string, error) {
	library := ar.NewReader(file)
	for {
		header, err := library.Next()
		if err == io.EOF {
			return nil, fmt.Errorf("unable to find data.tar.* part in %s", packageFile)
		}
		if err != nil {
			return nil, errors.Wrapf(err, "unable to read .deb archive from %s", packageFile)
		}

		if strings.HasPrefix(header.Name, "data.tar") {
			bufReader := bufio.NewReader(library)
			signature, err := bufReader.Peek(270)

			var isTar bool
			if err == nil {
				isTar = matchers.Tar(signature)
			}

			var tarInput io.Reader

			switch header.Name {
			case "data.tar":
				tarInput = bufReader
			case "data.tar.gz":
				if isTar {
					tarInput = bufReader
				} else {
					ungzip, err := gzip.NewReader(bufReader)
					if err != nil {
						return nil, errors.Wrapf(err, "unable to ungzip data.tar.gz from %s", packageFile)
					}
					defer func() { _ = ungzip.Close() }()
					tarInput = ungzip
				}
			case "data.tar.bz2":
				tarInput = bzip2.NewReader(bufReader)
			case "data.tar.xz":
				unxz, err := xz.NewReader(bufReader)
				if err != nil {
					return nil, errors.Wrapf(err, "unable to unxz data.tar.xz from %s", packageFile)
				}
				defer func() { _ = unxz.Close() }()
				tarInput = unxz
			case "data.tar.lzma":
				unlzma := lzma.NewReader(bufReader)
				defer func() { _ = unlzma.Close() }()
				tarInput = unlzma
			case "data.tar.zst":
				unzstd, err := zstd.NewReader(bufReader)
				if err != nil {
					return nil, errors.Wrapf(err, "unable to unzstd %s from %s", header.Name, packageFile)
				}
				defer unzstd.Close()
				tarInput = unzstd
			default:
				return nil, fmt.Errorf("unsupported tar compression in %s: %s", packageFile, header.Name)
			}

			untar := tar.NewReader(tarInput)
			var results []string
			for {
				tarHeader, err := untar.Next()
				if err == io.EOF {
					return results, nil
				}
				if err != nil {
					return nil, errors.Wrapf(err, "unable to read .tar archive from %s", packageFile)
				}

				if tarHeader.Typeflag == tar.TypeDir {
					continue
				}

				tarHeader.Name = strings.TrimPrefix(tarHeader.Name[2:], "./")
				results = append(results, tarHeader.Name)
			}
		}
	}
}
