package util

import (
	"bytes"
	"errors"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"strings"
	"time"

	"github.com/containers/common/libnetwork/types"
	"github.com/fsnotify/fsnotify"
	"github.com/sirupsen/logrus"
)

const (
	UnknownPackage = "Unknown"
)

var ErrInterrupt = errors.New("interrupted")

// Note: This function is copied from containers/podman libpod/util.go
// Please see https://github.com/containers/common/pull/1460
func queryPackageVersion(cmdArg ...string) string {
	output := UnknownPackage
	if 1 < len(cmdArg) {
		cmd := exec.Command(cmdArg[0], cmdArg[1:]...)
		if outp, err := cmd.Output(); err == nil {
			output = string(outp)
			deb := false
			if cmdArg[0] == "/usr/bin/dlocate" {
				// can return multiple matches
				l := strings.Split(output, "\n")
				output = l[0]
				deb = true
			} else if cmdArg[0] == "/usr/bin/dpkg" {
				deb = true
			}
			if deb {
				r := strings.Split(output, ": ")
				queryFormat := `${Package}_${Version}_${Architecture}`
				cmd = exec.Command("/usr/bin/dpkg-query", "-f", queryFormat, "-W", r[0])
				if outp, err := cmd.Output(); err == nil {
					output = string(outp)
				}
			}
		}
		if cmdArg[0] == "/sbin/apk" {
			prefix := cmdArg[len(cmdArg)-1] + " is owned by "
			output = strings.Replace(output, prefix, "", 1)
		}
	}
	return strings.Trim(output, "\n")
}

// Note: This function is copied from containers/podman libpod/util.go
// Please see https://github.com/containers/common/pull/1460
func PackageVersion(program string) string { // program is full path
	_, err := os.Stat(program)
	if err != nil {
		return UnknownPackage
	}
	packagers := [][]string{
		{"/usr/bin/rpm", "-q", "-f"},
		{"/usr/bin/dlocate", "-F"},             // Debian, Ubuntu (quick)
		{"/usr/bin/dpkg", "-S"},                // Debian, Ubuntu (slow)
		{"/usr/bin/pacman", "-Qo"},             // Arch
		{"/usr/bin/qfile", "-qv"},              // Gentoo (quick)
		{"/usr/bin/equery", "b"},               // Gentoo (slow)
		{"/sbin/apk", "info", "-W"},            // Alpine
		{"/usr/local/sbin/pkg", "which", "-q"}, // FreeBSD
	}

	for _, cmd := range packagers {
		cmd = append(cmd, program)
		if out := queryPackageVersion(cmd...); out != UnknownPackage {
			return out
		}
	}
	return UnknownPackage
}

// Note: This function is copied from containers/podman libpod/util.go
// Please see https://github.com/containers/common/pull/1460
func ProgramVersion(program string) (string, error) {
	return programVersion(program, false)
}

func ProgramVersionDnsname(program string) (string, error) {
	return programVersion(program, true)
}

func programVersion(program string, dnsname bool) (string, error) {
	cmd := exec.Command(program, "--version")
	var stdout bytes.Buffer
	var stderr bytes.Buffer
	cmd.Stdout = &stdout
	cmd.Stderr = &stderr

	err := cmd.Run()
	if err != nil {
		return "", fmt.Errorf("`%v --version` failed: %v %v (%v)", program, stderr.String(), stdout.String(), err)
	}

	output := strings.TrimSuffix(stdout.String(), "\n")
	// dnsname --version returns the information to stderr
	if dnsname {
		output = strings.TrimSuffix(stderr.String(), "\n")
	}

	return output, nil
}

// StringInSlice determines if a string is in a string slice, returns bool
func StringInSlice(s string, sl []string) bool {
	for _, i := range sl {
		if i == s {
			return true
		}
	}
	return false
}

// StringMatchRegexSlice determines if a given string matches one of the given regexes, returns bool
func StringMatchRegexSlice(s string, re []string) bool {
	for _, r := range re {
		m, err := regexp.MatchString(r, s)
		if err == nil && m {
			return true
		}
	}
	return false
}

// FilterID is a function used to compare an id against a set of ids, if the
// input is hex we check if the prefix matches. Otherwise we assume it is a
// regex and try to match that.
// see https://github.com/containers/podman/issues/18471 for why we do this
func FilterID(id string, filters []string) bool {
	for _, want := range filters {
		isRegex := types.NotHexRegex.MatchString(want)
		if isRegex {
			match, err := regexp.MatchString(want, id)
			if err == nil && match {
				return true
			}
		} else if strings.HasPrefix(id, strings.ToLower(want)) {
			return true
		}
	}
	return false
}

// WaitForFile waits until a file has been created or the given timeout has occurred
func WaitForFile(path string, chWait chan error, timeout time.Duration) (bool, error) {
	var inotifyEvents chan fsnotify.Event
	watcher, err := fsnotify.NewWatcher()
	if err == nil {
		if err := watcher.Add(filepath.Dir(path)); err == nil {
			inotifyEvents = watcher.Events
		}
		defer func() {
			if err := watcher.Close(); err != nil {
				logrus.Errorf("Failed to close fsnotify watcher: %v", err)
			}
		}()
	}

	var timeoutChan <-chan time.Time

	if timeout != 0 {
		timeoutChan = time.After(timeout)
	}

	for {
		select {
		case e := <-chWait:
			return true, e
		case <-inotifyEvents:
			_, err := os.Stat(path)
			if err == nil {
				return false, nil
			}
			if !os.IsNotExist(err) {
				return false, err
			}
		case <-time.After(25 * time.Millisecond):
			// Check periodically for the file existence.  It is needed
			// if the inotify watcher could not have been created.  It is
			// also useful when using inotify as if for any reasons we missed
			// a notification, we won't hang the process.
			_, err := os.Stat(path)
			if err == nil {
				return false, nil
			}
			if !os.IsNotExist(err) {
				return false, err
			}
		case <-timeoutChan:
			return false, fmt.Errorf("timed out waiting for file %s", path)
		}
	}
}
