//  Copyright 2019 Google Inc. All Rights Reserved.
//
//  Licensed under the Apache License, Version 2.0 (the "License");
//  you may not use this file except in compliance with the License.
//  You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.

package packages

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"os"
	"os/exec"
	"runtime"
	"strings"

	"github.com/GoogleCloudPlatform/osconfig/clog"
	"github.com/GoogleCloudPlatform/osconfig/osinfo"
	"github.com/GoogleCloudPlatform/osconfig/util"
)

var (
	dpkg      string
	dpkgQuery string
	dpkgDeb   string
	aptGet    string

	dpkgInstallArgs          = []string{"--install"}
	dpkgPackageFieldsMapping = map[string]string{
		"package":        "${Package}",
		"architecture":   "${Architecture}",
		"version":        "${Version}",
		"status":         "${db:Status-Status}",
		"source_name":    "${source:Package}",
		"source_version": "${source:Version}",
	}

	dpkgQueryArgs     = []string{"-W", "-f", formatFieldsMappingToFormattingString(dpkgPackageFieldsMapping)}
	dpkgRepairArgs    = []string{"--configure", "-a"}
	aptGetInstallArgs = []string{"install", "-y"}
	aptGetRemoveArgs  = []string{"remove", "-y"}
	aptGetUpdateArgs  = []string{"update"}

	aptGetUpgradeCmd     = "upgrade"
	aptGetFullUpgradeCmd = "full-upgrade"
	aptGetDistUpgradeCmd = "dist-upgrade"
	aptGetUpgradableArgs = []string{"--just-print", "-qq"}
	allowDowngradesArg   = "--allow-downgrades"

	dpkgErr = []byte("dpkg --configure -a")
)

func init() {
	if runtime.GOOS != "windows" {
		dpkg = "/usr/bin/dpkg"
		dpkgQuery = "/usr/bin/dpkg-query"
		dpkgDeb = "/usr/bin/dpkg-deb"
		aptGet = "/usr/bin/apt-get"
	}
	AptExists = util.Exists(aptGet)
	DpkgExists = util.Exists(dpkg)
	DpkgQueryExists = util.Exists(dpkgQuery)
}

// AptUpgradeType is the apt upgrade type.
type AptUpgradeType int

const (
	// AptGetUpgrade specifies apt-get upgrade should be run.
	AptGetUpgrade AptUpgradeType = iota
	// AptGetDistUpgrade specifies apt-get dist-upgrade should be run.
	AptGetDistUpgrade
	// AptGetFullUpgrade specifies apt-get full-upgrade should be run.
	AptGetFullUpgrade
)

type aptGetUpgradeOpts struct {
	upgradeType     AptUpgradeType
	showNew         bool
	allowDowngrades bool
}

// AptGetUpgradeOption is an option for apt-get upgrade.
type AptGetUpgradeOption func(*aptGetUpgradeOpts)

// AptGetUpgradeType returns a AptGetUpgradeOption that specifies upgrade type.
func AptGetUpgradeType(upgradeType AptUpgradeType) AptGetUpgradeOption {
	return func(args *aptGetUpgradeOpts) {
		args.upgradeType = upgradeType
	}
}

// AptGetUpgradeShowNew returns a AptGetUpgradeOption that indicates whether 'new' packages should be returned.
func AptGetUpgradeShowNew(showNew bool) AptGetUpgradeOption {
	return func(args *aptGetUpgradeOpts) {
		args.showNew = showNew
	}
}

// AptGetUpgradeAllowDowngrades returns a AptGetUpgradeOption that specifies AllowDowngrades.
func AptGetUpgradeAllowDowngrades(allowDowngrades bool) AptGetUpgradeOption {
	return func(args *aptGetUpgradeOpts) {
		args.allowDowngrades = allowDowngrades
	}
}

func dpkgRepair(ctx context.Context, out []byte) bool {
	// Error code 100 may occur for non repairable errors, just check the output.
	if !bytes.Contains(out, dpkgErr) {
		return false
	}
	clog.Debugf(ctx, "apt-get error, attempting dpkg repair.")
	// Ignore error here, just log and rerun apt-get.
	run(ctx, dpkg, dpkgRepairArgs)

	return true
}

type cmdModifier func(*exec.Cmd)

func runAptGet(ctx context.Context, args []string, cmdModifiers []cmdModifier) ([]byte, []byte, error) {
	cmd := exec.CommandContext(ctx, aptGet, args...)
	for _, modifier := range cmdModifiers {
		modifier(cmd)
	}

	return runner.Run(ctx, cmd)
}

func runAptGetWithDowngradeRetrial(ctx context.Context, args []string, cmdModifiers []cmdModifier) ([]byte, []byte, error) {
	stdout, stderr, err := runAptGet(ctx, args, cmdModifiers)
	if err != nil {
		if strings.Contains(string(stderr), "E: Packages were downgraded and -y was used without --allow-downgrades.") {
			cmdModifiers = append(cmdModifiers, func(cmd *exec.Cmd) {
				cmd.Args = append(cmd.Args, allowDowngradesArg)
			})
			stdout, stderr, err = runAptGet(ctx, args, cmdModifiers)
		}
	}

	return stdout, stderr, err
}

func parseDpkgDeb(data []byte) (*PkgInfo, error) {
	/*
	   new Debian package, version 2.0.
	   size 6731954 bytes: control archive=2138 bytes.
	       498 bytes,    12 lines      control
	      3465 bytes,    31 lines      md5sums
	      2793 bytes,    65 lines   *  postinst             #!/bin/sh
	       938 bytes,    28 lines   *  postrm               #!/bin/sh
	       216 bytes,     7 lines   *  prerm                #!/bin/sh
	   Package: google-guest-agent
	   Version: 1:1dummy-g1
	   Architecture: amd64
	   Maintainer: Google Cloud Team <gc-team@google.com>
	   Installed-Size: 23279
	   Depends: init-system-helpers (>= 1.18~)
	   Conflicts: python-google-compute-engine, python3-google-compute-engine
	   Section: misc
	   Priority: optional
	   Description: Google Compute Engine Guest Agent
	    Contains the guest agent and metadata script runner binaries.
	   Git: https://github.com/GoogleCloudPlatform/guest-agent/tree/c3d526e650c4e45ae3258c07836fd72f85fd9fc8
	*/

	lines := bytes.Split(bytes.TrimSpace(data), []byte("\n"))
	info := &PkgInfo{}
	for _, ln := range lines {
		if info.Name != "" && info.Version != "" && info.Arch != "" {
			break
		}
		fields := bytes.Fields(ln)
		if len(fields) != 2 {
			continue
		}
		if bytes.Contains(fields[0], []byte("Package:")) {
			// Some packages do not adhere to the Debian Policy and might have mix-cased names
			// And dpkg will register the package with lower case anyway so use lower-case package name
			// This is necessary because the compliance check is done between the .deb file descriptor value
			// and the internal dpkg db which register a lower-cased package name
			info.Name = strings.ToLower(string(fields[1]))
			continue
		}
		if bytes.Contains(fields[0], []byte("Version:")) {
			info.Version = string(fields[1])
			continue
		}
		if bytes.Contains(fields[0], []byte("Architecture:")) {
			info.Arch = osinfo.NormalizeArchitecture(string(fields[1]))
			continue
		}
	}
	if info.Name == "" || info.Version == "" || info.Arch == "" {
		return nil, fmt.Errorf("could not parse dpkg-deb output: %q", data)
	}
	return info, nil
}

// DebPkgInfo gets PkgInfo from a deb package.
func DebPkgInfo(ctx context.Context, path string) (*PkgInfo, error) {
	out, err := run(ctx, dpkgDeb, []string{"-I", path})
	if err != nil {
		return nil, err
	}

	return parseDpkgDeb(out)
}

// InstallAptPackages installs apt packages.
func InstallAptPackages(ctx context.Context, pkgs []string) error {
	args := append(aptGetInstallArgs, pkgs...)
	cmdModifiers := []cmdModifier{
		func(cmd *exec.Cmd) {
			cmd.Env = append(os.Environ(), "DEBIAN_FRONTEND=noninteractive")
		},
	}
	stdout, stderr, err := runAptGetWithDowngradeRetrial(ctx, args, cmdModifiers)
	if err != nil {
		if dpkgRepair(ctx, stderr) {
			stdout, stderr, err = runAptGetWithDowngradeRetrial(ctx, args, cmdModifiers)
		}
	}
	if err != nil {
		err = fmt.Errorf("error running %s with args %q: %v, stdout: %q, stderr: %q", aptGet, args, err, stdout, stderr)
	}
	return err
}

// RemoveAptPackages removes apt packages.
func RemoveAptPackages(ctx context.Context, pkgs []string) error {
	args := append(aptGetRemoveArgs, pkgs...)
	cmdModifiers := []cmdModifier{
		func(cmd *exec.Cmd) {
			cmd.Env = append(os.Environ(), "DEBIAN_FRONTEND=noninteractive")
		},
	}
	stdout, stderr, err := runAptGet(ctx, args, cmdModifiers)
	if err != nil {
		if dpkgRepair(ctx, stderr) {
			stdout, stderr, err = runAptGet(ctx, args, cmdModifiers)
		}
	}
	if err != nil {
		err = fmt.Errorf("error running %s with args %q: %v, stdout: %q, stderr: %q", aptGet, args, err, stdout, stderr)
	}
	return err
}

func parseAptUpdates(ctx context.Context, data []byte, showNew bool) []*PkgInfo {
	/*
		Inst libldap-common [2.4.45+dfsg-1ubuntu1.2] (2.4.45+dfsg-1ubuntu1.3 Ubuntu:18.04/bionic-updates, Ubuntu:18.04/bionic-security [all])
		Inst firmware-linux-free (3.4 Debian:9.9/stable [all]) []
		Inst google-cloud-sdk [245.0.0-0] (246.0.0-0 cloud-sdk-stretch:cloud-sdk-stretch [all])
		Inst linux-image-4.9.0-9-amd64 (4.9.168-1+deb9u2 Debian-Security:9/stable [amd64])
		Inst linux-image-amd64 [4.9+80+deb9u6] (4.9+80+deb9u7 Debian:9.9/stable [amd64])
		Conf firmware-linux-free (3.4 Debian:9.9/stable [all])
		Conf google-cloud-sdk (246.0.0-0 cloud-sdk-stretch:cloud-sdk-stretch [all])
		Conf linux-image-4.9.0-9-amd64 (4.9.168-1+deb9u2 Debian-Security:9/stable [amd64])
		Conf linux-image-amd64 (4.9+80+deb9u7 Debian:9.9/stable [amd64])
	*/

	lines := bytes.Split(bytes.TrimSpace(data), []byte("\n"))

	var pkgs []*PkgInfo
	for _, ln := range lines {
		pkg := bytes.Fields(ln)
		if len(pkg) < 5 || string(pkg[0]) != "Inst" {
			continue
		}
		// Inst google-cloud-sdk [245.0.0-0] (246.0.0-0 cloud-sdk-stretch:cloud-sdk-stretch [all])
		pkg = pkg[1:] // ==> google-cloud-sdk [245.0.0-0] (246.0.0-0 cloud-sdk-stretch:cloud-sdk-stretch [all])
		if bytes.HasPrefix(pkg[1], []byte("[")) {
			pkg = append(pkg[:1], pkg[2:]...) // ==> google-cloud-sdk (246.0.0-0 cloud-sdk-stretch:cloud-sdk-stretch [all])
		} else if !showNew {
			// This is a newly installed package and not an upgrade, ignore if showNew is false.
			continue
		}
		// Drop trailing "[]" if they exist.
		if bytes.Contains(pkg[len(pkg)-1], []byte("[]")) {
			pkg = pkg[:len(pkg)-1]
		}
		if !bytes.HasPrefix(pkg[1], []byte("(")) || !bytes.HasSuffix(pkg[len(pkg)-1], []byte(")")) {
			continue
		}
		ver := bytes.Trim(pkg[1], "(")             // (246.0.0-0 => 246.0.0-0
		arch := bytes.Trim(pkg[len(pkg)-1], "[])") // [all]) => all
		pkgs = append(pkgs, &PkgInfo{Name: string(pkg[0]), Arch: osinfo.NormalizeArchitecture(string(arch)), Version: string(ver)})
	}
	return pkgs
}

// AptUpdates returns all the packages that will be installed when running
// apt-get [dist-|full-]upgrade.
func AptUpdates(ctx context.Context, opts ...AptGetUpgradeOption) ([]*PkgInfo, error) {
	aptOpts := &aptGetUpgradeOpts{
		upgradeType:     AptGetUpgrade,
		showNew:         false,
		allowDowngrades: false,
	}

	for _, opt := range opts {
		opt(aptOpts)
	}

	args := aptGetUpgradableArgs
	switch aptOpts.upgradeType {
	case AptGetUpgrade:
		args = append(aptGetUpgradableArgs, aptGetUpgradeCmd)
	case AptGetDistUpgrade:
		args = append(aptGetUpgradableArgs, aptGetDistUpgradeCmd)
	case AptGetFullUpgrade:
		args = append(aptGetUpgradableArgs, aptGetFullUpgradeCmd)
	default:
		return nil, fmt.Errorf("unknown upgrade type: %q", aptOpts.upgradeType)
	}

	if _, err := AptUpdate(ctx); err != nil {
		return nil, err
	}

	out, _, err := runAptGetWithDowngradeRetrial(ctx, args, []cmdModifier{
		func(cmd *exec.Cmd) {
			cmd.Env = append(os.Environ(), "DEBIAN_FRONTEND=noninteractive")
		},
	})
	if err != nil {
		return nil, err
	}

	return parseAptUpdates(ctx, out, aptOpts.showNew), nil
}

// AptUpdate runs apt-get update.
func AptUpdate(ctx context.Context) ([]byte, error) {
	stdout, _, err := runAptGet(ctx, aptGetUpdateArgs, []cmdModifier{
		func(cmd *exec.Cmd) {
			cmd.Env = append(os.Environ(), "DEBIAN_FRONTEND=noninteractive")
		},
	})
	return stdout, err
}

// InstalledDebPackages queries for all installed deb packages.
func InstalledDebPackages(ctx context.Context) ([]*PkgInfo, error) {
	out, err := run(ctx, dpkgQuery, dpkgQueryArgs)
	if err != nil {
		return nil, err
	}

	return parseInstalledDebPackages(ctx, out), nil
}

func parseInstalledDebPackages(ctx context.Context, data []byte) []*PkgInfo {
	/*
		Each line contains an entry in a json format, keep in mind that whole output is not valid json.

		{"package":"adduser","architecture":"all","version":"3.118ubuntu2","status":"installed","source_name":"adduser","source_version":"3.118ubuntu2"}
		{"package":"apt-utils","architecture":"amd64","version":"2.0.10","status":"installed","source_name":"apt","source_version":"2.0.10"}
		{"package":"git","architecture":"amd64","version":"1:2.25.1-1ubuntu3.12","status":"installed","source_name":"git","source_version":"1:2.25.1-1ubuntu3.12"}
		...
	*/
	entries := bytes.Split(bytes.TrimSpace(data), []byte("\n"))

	var result []*PkgInfo
	for _, entry := range entries {
		var dpkg packageMetadata
		if err := json.Unmarshal(entry, &dpkg); err != nil {
			clog.Debugf(ctx, "unable to parse dpkg package info, err %s, raw - %s", err, string(entry))
			continue
		}

		pkg := pkgInfoFromPackageMetadata(dpkg)
		if dpkg.Status != "installed" {
			continue
		}

		result = append(result, pkg)
	}

	return result
}

// DpkgInstall installs a deb package.
func DpkgInstall(ctx context.Context, path string) error {
	_, err := run(ctx, dpkg, append(dpkgInstallArgs, path))
	return err
}
