//  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 ospatch

import (
	"bufio"
	"bytes"
	"errors"
	"fmt"
	"os"
	"os/exec"
	"strconv"

	"github.com/GoogleCloudPlatform/osconfig/packages"
)

const (
	rpmquery = "/usr/bin/rpmquery"
)

func getBtime(stat string) (int64, error) {
	f, err := os.Open(stat)
	if err != nil {
		return 0, fmt.Errorf("error opening %s: %v", stat, err)
	}
	defer f.Close()

	var btime int64
	scnr := bufio.NewScanner(f)
	for scnr.Scan() {
		if bytes.HasPrefix(scnr.Bytes(), []byte("btime")) {
			split := bytes.SplitN(scnr.Bytes(), []byte(" "), 2)
			if len(split) != 2 {
				return 0, fmt.Errorf("error parsing btime from %s: %q", stat, scnr.Text())
			}
			btime, err = strconv.ParseInt(string(bytes.TrimSpace(split[1])), 10, 64)
			if err != nil {
				return 0, fmt.Errorf("error parsing btime: %v", err)
			}
			break
		}
	}
	if err := scnr.Err(); err != nil && btime == 0 {
		return 0, fmt.Errorf("error scanning %s: %v", stat, err)
	}
	if btime == 0 {
		return 0, fmt.Errorf("could not find btime in %s", stat)
	}

	return btime, nil
}

func rpmRebootRequired(pkgs []byte, btime int64) bool {
	// Scanning this output is best effort, false negatives are much prefered
	// to false positives, and keeping this as simple as possible is
	// beneficial.
	scnr := bufio.NewScanner(bytes.NewReader(pkgs))
	for scnr.Scan() {
		itime, err := strconv.ParseInt(scnr.Text(), 10, 64)
		if err != nil {
			continue
		}
		if itime > btime {
			return true
		}
	}

	return false
}

// rpmReboot returns whether an rpm based system should reboot in order to
// finish installing updates.
// To get this signal we look at a set of well known packages and whether
// install time > system boot time. This list is not meant to be exhastive,
// just to provide a signal when core system packages are updated.
func rpmReboot() (bool, error) {
	provides := []string{
		// Common packages.
		"kernel", "glibc", "gnutls",
		// EL packages.
		"linux-firmware", "openssl-libs", "dbus",
		// Suse packages.
		"kernel-firmware", "libopenssl1_1", "libopenssl1_0_0", "dbus-1",
	}
	args := append([]string{"--queryformat", "%{INSTALLTIME}\n", "--whatprovides"}, provides...)
	out, err := exec.Command(rpmquery, args...).Output()
	if err != nil {
		// We don't care about return codes as we know some of these packages won't be installed.
		if _, ok := err.(*exec.ExitError); !ok {
			return false, fmt.Errorf("error running %s: %v", rpmquery, err)
		}
	}

	btime, err := getBtime("/proc/stat")
	if err != nil {
		return false, err
	}

	return rpmRebootRequired(out, btime), nil
}

func shouldPackageBeExcluded(excludes []*Exclude, packageName *string) bool {
	for _, exclude := range excludes {
		if exclude.MatchesName(packageName) {
			return true
		}
	}
	return false
}

func containsString(ss []string, c string) bool {
	for _, s := range ss {
		if s == c {
			return true
		}
	}
	return false
}

func filterPackages(pkgs []*packages.PkgInfo, exclusivePackages []string, excludes []*Exclude) ([]*packages.PkgInfo, error) {
	if len(exclusivePackages) != 0 && len(excludes) != 0 {
		return nil, errors.New("exclusivePackages and excludes can not both be non 0")
	}
	var fPkgs = []*packages.PkgInfo{}
	for _, pkg := range pkgs {
		if shouldPackageBeExcluded(excludes, &pkg.Name) {
			continue
		}
		if exclusivePackages == nil || containsString(exclusivePackages, pkg.Name) {
			fPkgs = append(fPkgs, pkg)
		}
	}
	return fPkgs, nil
}
