//  Copyright 2017 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 provides package management functions for Windows and Linux
// systems.
package packages

import (
	"context"
	"fmt"
	"os/exec"
	"sort"
	"strings"
	"time"

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

var (
	// AptExists indicates whether apt is installed.
	AptExists bool
	// DpkgExists indicates whether dpkg is installed.
	DpkgExists bool
	// DpkgQueryExists indicates whether dpkg-query is installed.
	DpkgQueryExists bool
	// YumExists indicates whether yum is installed.
	YumExists bool
	// ZypperExists indicates whether zypper is installed.
	ZypperExists bool
	// RPMExists indicates whether rpm is installed.
	RPMExists bool
	// RPMQueryExists indicates whether rpmquery is installed.
	RPMQueryExists bool
	// COSPkgInfoExists indicates whether COS package information is available.
	COSPkgInfoExists bool
	// GemExists indicates whether gem is installed.
	GemExists bool
	// PipExists indicates whether pip is installed.
	PipExists bool
	// GooGetExists indicates whether googet is installed.
	GooGetExists bool
	// MSIExists indicates whether MSIs can be installed.
	MSIExists bool

	noarch = osinfo.NormalizeArchitecture("noarch")

	runner = util.CommandRunner(&util.DefaultRunner{})

	ptyrunner = util.CommandRunner(&ptyRunner{})
)

// PackageUpdatesProvider define contract to extract available updates from the VM.
type PackageUpdatesProvider interface {
	GetPackageUpdates(context.Context) (Packages, error)
}

// InstalledPackagesProvider define contract to extract installed packages from the VM.
type InstalledPackagesProvider interface {
	GetInstalledPackages(context.Context) (Packages, error)
}

type defaultUpdatesProvider struct{}

// NewPackageUpdatesProvider return fully initialize provider.
func NewPackageUpdatesProvider() PackageUpdatesProvider {
	return defaultUpdatesProvider{}
}

func (p defaultUpdatesProvider) GetPackageUpdates(ctx context.Context) (Packages, error) {
	return GetPackageUpdates(ctx)
}

type defaultInstalledPackagesProvider struct{}

func (p defaultInstalledPackagesProvider) GetInstalledPackages(ctx context.Context) (Packages, error) {
	return GetInstalledPackages(ctx)
}

// Packages is a selection of packages based on their manager.
type Packages struct {
	Yum                []*PkgInfo            `json:"yum,omitempty"`
	Rpm                []*PkgInfo            `json:"rpm,omitempty"`
	Apt                []*PkgInfo            `json:"apt,omitempty"`
	Deb                []*PkgInfo            `json:"deb,omitempty"`
	Zypper             []*PkgInfo            `json:"zypper,omitempty"`
	ZypperPatches      []*ZypperPatch        `json:"zypperPatches,omitempty"`
	COS                []*PkgInfo            `json:"cos,omitempty"`
	Gem                []*PkgInfo            `json:"gem,omitempty"`
	Pip                []*PkgInfo            `json:"pip,omitempty"`
	GooGet             []*PkgInfo            `json:"googet,omitempty"`
	WUA                []*WUAPackage         `json:"wua,omitempty"`
	QFE                []*QFEPackage         `json:"qfe,omitempty"`
	WindowsApplication []*WindowsApplication `json:"-"`
}

// PkgInfo describes a package.
type PkgInfo struct {
	Name, Arch, RawArch, Version string

	Source Source
}

// Source represents source package from which binary package was built.
type Source struct {
	Name, Version string
}

func (i *PkgInfo) String() string {
	return fmt.Sprintf("%s %s %s", i.Name, i.Arch, i.Version)
}

// ZypperPatch describes a Zypper patch.
type ZypperPatch struct {
	Name, Category, Severity, Summary string
}

// WUAPackage describes a Windows Update Agent package.
type WUAPackage struct {
	LastDeploymentChangeTime time.Time
	Title                    string
	Description              string
	SupportURL               string
	UpdateID                 string
	Categories               []string
	KBArticleIDs             []string
	MoreInfoURLs             []string
	CategoryIDs              []string
	RevisionNumber           int32
}

// QFEPackage describes a Windows Quick Fix Engineering package.
type QFEPackage struct {
	Caption, Description, HotFixID, InstalledOn string
}

// WindowsApplication describes a Windows Application.
type WindowsApplication struct {
	DisplayName    string
	DisplayVersion string
	InstallDate    time.Time
	Publisher      string
	HelpLink       string
}

func run(ctx context.Context, cmd string, args []string) ([]byte, error) {
	stdout, stderr, err := runner.Run(ctx, exec.CommandContext(ctx, cmd, args...))
	if err != nil {
		return nil, fmt.Errorf("error running %s with args %q: %v, stdout: %q, stderr: %q", cmd, args, err, stdout, stderr)
	}
	return stdout, nil
}

func runWithDeadline(ctx context.Context, timeout time.Duration, cmd string, args []string) ([]byte, error) {
	ctxWithTimeout, cancel := context.WithTimeout(ctx, timeout)
	defer cancel()
	return run(ctxWithTimeout, cmd, args)
}

func formatFieldsMappingToFormattingString(fieldsMapping map[string]string) string {
	fieldsDescriptors := make([]string, 0, len(fieldsMapping))

	for name, selector := range fieldsMapping {
		// Format field name and its selector to one single entry separated by ":" and each of them wrapped in quotes
		// Examples:
		// name:source_name, selector:${source:Package -> ""source_name":"${source:Package}"".
		// name:source_name, selector:%{NAME} -> ""source_name":"%{NAME}"".
		fieldsDescriptors = append(fieldsDescriptors, fmt.Sprintf("\"%s\":\"%s\"", name, selector))
	}

	// Sort descriptors to get predictable result.
	sort.Strings(fieldsDescriptors)

	// Returns string to format all information in json
	// Example: {"package":"${Package}","architecture":"${Architecture}","version":"${Version}","status":"${db:Status-Status}"...}\n
	// See dpkgInfoFieldsMapping for full set of fields.
	return "\\{" + strings.Join(fieldsDescriptors, ",") + "\\}\n"
}

type packageMetadata struct {
	Package       string `json:"package"`
	Architecture  string `json:"architecture"`
	Version       string `json:"version"`
	Status        string `json:"status"`
	SourceName    string `json:"source_name"`
	SourceVersion string `json:"source_version"`
}

func pkgInfoFromPackageMetadata(pm packageMetadata) *PkgInfo {
	return &PkgInfo{
		Name:    pm.Package,
		Arch:    osinfo.NormalizeArchitecture(pm.Architecture),
		Version: pm.Version,
		Source: Source{
			Name:    pm.SourceName,
			Version: pm.SourceVersion,
		},
	}
}

type ptyRunner struct{}

func (p *ptyRunner) Run(ctx context.Context, cmd *exec.Cmd) ([]byte, []byte, error) {
	clog.Debugf(ctx, "Running %q with args %q\n", cmd.Path, cmd.Args[1:])
	stdout, stderr, err := runWithPty(cmd)
	clog.Debugf(ctx, "%s %q output:\n%s", cmd.Path, cmd.Args[1:], strings.ReplaceAll(string(stdout), "\n", "\n "))
	return stdout, stderr, err
}

// SetCommandRunner allows external clients to set a custom commandRunner.
func SetCommandRunner(commandRunner util.CommandRunner) {
	runner = commandRunner
}

// SetPtyCommandRunner allows external clients to set a custom
// custom commandRunner.
func SetPtyCommandRunner(commandRunner util.CommandRunner) {
	ptyrunner = commandRunner
}
