package agentendpoint

import (
	"bytes"
	"context"
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"io"
	"reflect"
	"sort"
	"strings"
	"time"

	"github.com/GoogleCloudPlatform/osconfig/agentconfig"
	"github.com/GoogleCloudPlatform/osconfig/attributes"
	"github.com/GoogleCloudPlatform/osconfig/clog"
	"github.com/GoogleCloudPlatform/osconfig/inventory"
	"github.com/GoogleCloudPlatform/osconfig/packages"
	"github.com/GoogleCloudPlatform/osconfig/retryutil"
	"google.golang.org/protobuf/proto"
	"google.golang.org/protobuf/types/known/timestamppb"

	"cloud.google.com/go/osconfig/agentendpoint/apiv1/agentendpointpb"
	datepb "google.golang.org/genproto/googleapis/type/date"
)

const (
	inventoryURL = agentconfig.ReportURL + "/guestInventory"
)

// ReportInventory writes inventory to guest attributes and reports it to agent endpoint.
func (c *Client) ReportInventory(ctx context.Context) {
	state := c.inventoryProvider.Get(ctx)

	if agentconfig.GuestAttributesEnabled() && !agentconfig.DisableInventoryWrite() {
		clog.Infof(ctx, "Writing inventory to guest attributes")
		write(ctx, state, inventoryURL)
	}

	c.report(ctx, state)
}

func write(ctx context.Context, state *inventory.InstanceInventory, url string) {
	clog.Debugf(ctx, "Writing instance inventory to guest attributes.")

	e := reflect.ValueOf(state).Elem()
	t := e.Type()
	for i := 0; i < e.NumField(); i++ {
		f := e.Field(i)
		u := fmt.Sprintf("%s/%s", url, t.Field(i).Name)
		switch f.Kind() {
		case reflect.String:
			clog.Debugf(ctx, "postAttribute %s: %+v", u, f)
			if err := attributes.PostAttribute(u, strings.NewReader(f.String())); err != nil {
				clog.Errorf(ctx, "postAttribute error: %v", err)
			}
		case reflect.Ptr:
			switch reflect.Indirect(f).Kind() {
			case reflect.Struct:
				clog.Debugf(ctx, "postAttributeCompressed %s", u)
				if err := attributes.PostAttributeCompressed(u, f.Interface()); err != nil {
					clog.Errorf(ctx, "postAttributeCompressed error: %v", err)
				}
			}
		}
	}
}

func (c *Client) report(ctx context.Context, state *inventory.InstanceInventory) {
	clog.Debugf(ctx, "Reporting instance inventory to agent endpoint.")
	inventory := formatInventory(ctx, state)

	reportFull := false
	var res *agentendpointpb.ReportInventoryResponse
	var err error
	f := func() error {
		res, err = c.reportInventory(ctx, inventory, reportFull)
		if err != nil {
			return err
		}
		return nil
	}

	if err = retryutil.RetryAPICall(ctx, apiRetrySec*time.Second, "ReportInventory", f); err != nil {
		clog.Errorf(ctx, "Error reporting inventory checksum: %v", err)
		return
	}

	if res.GetReportFullInventory() {
		reportFull = true
		if err = retryutil.RetryAPICall(ctx, apiRetrySec*time.Second, "ReportInventory", f); err != nil {
			clog.Errorf(ctx, "Error reporting full inventory: %v", err)
			return
		}
	}
}

func formatInventory(ctx context.Context, state *inventory.InstanceInventory) *agentendpointpb.Inventory {
	osInfo := &agentendpointpb.Inventory_OsInfo{
		Hostname:             state.Hostname,
		LongName:             state.LongName,
		ShortName:            state.ShortName,
		Version:              state.Version,
		Architecture:         state.Architecture,
		KernelVersion:        state.KernelVersion,
		KernelRelease:        state.KernelRelease,
		OsconfigAgentVersion: state.OSConfigAgentVersion,
	}
	installedPackages := formatPackages(ctx, state.InstalledPackages, state.ShortName)
	availablePackages := formatPackages(ctx, state.PackageUpdates, state.ShortName)

	return &agentendpointpb.Inventory{OsInfo: osInfo, InstalledPackages: installedPackages, AvailablePackages: availablePackages}
}

func formatPackages(ctx context.Context, pkgs *packages.Packages, shortName string) []*agentendpointpb.Inventory_SoftwarePackage {
	var softwarePackages []*agentendpointpb.Inventory_SoftwarePackage
	if pkgs == nil {
		return softwarePackages
	}
	if pkgs.Apt != nil {
		temp := make([]*agentendpointpb.Inventory_SoftwarePackage, len(pkgs.Apt))
		for i, pkg := range pkgs.Apt {
			temp[i] = &agentendpointpb.Inventory_SoftwarePackage{
				Details: formatAptPackage(pkg),
			}
		}
		softwarePackages = append(softwarePackages, temp...)
	}
	if pkgs.Deb != nil {
		temp := make([]*agentendpointpb.Inventory_SoftwarePackage, len(pkgs.Deb))
		for i, pkg := range pkgs.Deb {
			temp[i] = &agentendpointpb.Inventory_SoftwarePackage{
				Details: formatAptPackage(pkg),
			}
		}
		softwarePackages = append(softwarePackages, temp...)
	}
	if pkgs.GooGet != nil {
		temp := make([]*agentendpointpb.Inventory_SoftwarePackage, len(pkgs.GooGet))
		for i, pkg := range pkgs.GooGet {
			temp[i] = &agentendpointpb.Inventory_SoftwarePackage{
				Details: formatGooGetPackage(pkg),
			}
		}
		softwarePackages = append(softwarePackages, temp...)
	}
	if pkgs.Yum != nil {
		temp := make([]*agentendpointpb.Inventory_SoftwarePackage, len(pkgs.Yum))
		for i, pkg := range pkgs.Yum {
			temp[i] = &agentendpointpb.Inventory_SoftwarePackage{
				Details: formatYumPackage(pkg),
			}
		}
		softwarePackages = append(softwarePackages, temp...)
	}
	if pkgs.Zypper != nil {
		temp := make([]*agentendpointpb.Inventory_SoftwarePackage, len(pkgs.Zypper))
		for i, pkg := range pkgs.Zypper {
			temp[i] = &agentendpointpb.Inventory_SoftwarePackage{
				Details: formatZypperPackage(pkg),
			}
		}
		softwarePackages = append(softwarePackages, temp...)
	}
	if pkgs.Rpm != nil {
		temp := make([]*agentendpointpb.Inventory_SoftwarePackage, len(pkgs.Rpm))
		if packages.YumExists || !packages.ZypperExists {
			for i, pkg := range pkgs.Rpm {
				temp[i] = &agentendpointpb.Inventory_SoftwarePackage{
					Details: formatYumPackage(pkg),
				}
			}
		} else {
			for i, pkg := range pkgs.Rpm {
				temp[i] = &agentendpointpb.Inventory_SoftwarePackage{
					Details: formatZypperPackage(pkg),
				}
			}
		}
		softwarePackages = append(softwarePackages, temp...)
	}
	if pkgs.ZypperPatches != nil {
		temp := make([]*agentendpointpb.Inventory_SoftwarePackage, len(pkgs.ZypperPatches))
		for i, pkg := range pkgs.ZypperPatches {
			temp[i] = &agentendpointpb.Inventory_SoftwarePackage{
				Details: formatZypperPatch(pkg),
			}
		}
		softwarePackages = append(softwarePackages, temp...)
	}
	if pkgs.WUA != nil {
		temp := make([]*agentendpointpb.Inventory_SoftwarePackage, len(pkgs.WUA))
		for i, pkg := range pkgs.WUA {
			temp[i] = &agentendpointpb.Inventory_SoftwarePackage{
				Details: formatWUAPackage(pkg),
			}
		}
		softwarePackages = append(softwarePackages, temp...)
	}
	if pkgs.QFE != nil {
		temp := make([]*agentendpointpb.Inventory_SoftwarePackage, len(pkgs.QFE))
		for i, pkg := range pkgs.QFE {
			temp[i] = &agentendpointpb.Inventory_SoftwarePackage{
				Details: formatQFEPackage(ctx, pkg),
			}
		}
		softwarePackages = append(softwarePackages, temp...)
	}
	if pkgs.COS != nil {
		temp := make([]*agentendpointpb.Inventory_SoftwarePackage, len(pkgs.COS))
		for i, pkg := range pkgs.COS {
			temp[i] = &agentendpointpb.Inventory_SoftwarePackage{
				Details: formatCOSPackage(pkg),
			}
		}
		softwarePackages = append(softwarePackages, temp...)
	}
	if pkgs.WindowsApplication != nil {
		temp := make([]*agentendpointpb.Inventory_SoftwarePackage, len(pkgs.WindowsApplication))
		for i, pkg := range pkgs.WindowsApplication {
			temp[i] = &agentendpointpb.Inventory_SoftwarePackage{
				Details: formatWindowsApplication(pkg),
			}
		}
		softwarePackages = append(softwarePackages, temp...)
	}
	// Ignore Pip and Gem packages.

	return softwarePackages
}

func formatAptPackage(pkg *packages.PkgInfo) *agentendpointpb.Inventory_SoftwarePackage_AptPackage {
	fPkg := &agentendpointpb.Inventory_SoftwarePackage_AptPackage{
		AptPackage: &agentendpointpb.Inventory_VersionedPackage{
			PackageName:  pkg.Name,
			Architecture: pkg.Arch,
			Version:      pkg.Version,
		},
	}

	// for some of the APT packages source package might be available.
	if pkg.Source.Name != "" {
		fPkg.AptPackage.Source = &agentendpointpb.Inventory_VersionedPackage_Source{
			Name:    pkg.Source.Name,
			Version: pkg.Source.Version,
		}
	}

	return fPkg
}

func formatCOSPackage(pkg *packages.PkgInfo) *agentendpointpb.Inventory_SoftwarePackage_CosPackage {
	return &agentendpointpb.Inventory_SoftwarePackage_CosPackage{
		CosPackage: &agentendpointpb.Inventory_VersionedPackage{
			PackageName:  pkg.Name,
			Architecture: pkg.Arch,
			Version:      pkg.Version,
		}}
}

func formatGooGetPackage(pkg *packages.PkgInfo) *agentendpointpb.Inventory_SoftwarePackage_GoogetPackage {
	return &agentendpointpb.Inventory_SoftwarePackage_GoogetPackage{
		GoogetPackage: &agentendpointpb.Inventory_VersionedPackage{
			PackageName:  pkg.Name,
			Architecture: pkg.Arch,
			Version:      pkg.Version,
		}}
}

func formatYumPackage(pkg *packages.PkgInfo) *agentendpointpb.Inventory_SoftwarePackage_YumPackage {
	fPkg := &agentendpointpb.Inventory_SoftwarePackage_YumPackage{
		YumPackage: &agentendpointpb.Inventory_VersionedPackage{
			PackageName:  pkg.Name,
			Architecture: pkg.Arch,
			Version:      pkg.Version,
		},
	}

	// for some of the YUM packages source package might be available.
	if pkg.Source.Name != "" {
		fPkg.YumPackage.Source = &agentendpointpb.Inventory_VersionedPackage_Source{
			Name:    pkg.Source.Name,
			Version: pkg.Source.Version,
		}
	}

	return fPkg
}

func formatZypperPackage(pkg *packages.PkgInfo) *agentendpointpb.Inventory_SoftwarePackage_ZypperPackage {
	return &agentendpointpb.Inventory_SoftwarePackage_ZypperPackage{
		ZypperPackage: &agentendpointpb.Inventory_VersionedPackage{
			PackageName:  pkg.Name,
			Architecture: pkg.Arch,
			Version:      pkg.Version}}
}

func formatZypperPatch(pkg *packages.ZypperPatch) *agentendpointpb.Inventory_SoftwarePackage_ZypperPatch {
	return &agentendpointpb.Inventory_SoftwarePackage_ZypperPatch{
		ZypperPatch: &agentendpointpb.Inventory_ZypperPatch{
			PatchName: pkg.Name,
			Category:  pkg.Category,
			Severity:  pkg.Severity,
			Summary:   pkg.Summary,
		}}
}

func formatWUAPackage(pkg *packages.WUAPackage) *agentendpointpb.Inventory_SoftwarePackage_WuaPackage {
	var categories []*agentendpointpb.Inventory_WindowsUpdatePackage_WindowsUpdateCategory
	for idx, category := range pkg.Categories {
		categories = append(categories, &agentendpointpb.Inventory_WindowsUpdatePackage_WindowsUpdateCategory{
			Id:   pkg.CategoryIDs[idx],
			Name: category,
		})
	}

	return &agentendpointpb.Inventory_SoftwarePackage_WuaPackage{
		WuaPackage: &agentendpointpb.Inventory_WindowsUpdatePackage{
			Title:                    pkg.Title,
			Description:              pkg.Description,
			Categories:               categories,
			KbArticleIds:             pkg.KBArticleIDs,
			SupportUrl:               pkg.SupportURL,
			MoreInfoUrls:             pkg.MoreInfoURLs,
			UpdateId:                 pkg.UpdateID,
			RevisionNumber:           pkg.RevisionNumber,
			LastDeploymentChangeTime: timestamppb.New(pkg.LastDeploymentChangeTime),
		}}
}

func formatQFEPackage(ctx context.Context, pkg *packages.QFEPackage) *agentendpointpb.Inventory_SoftwarePackage_QfePackage {
	installedTime, err := time.Parse("1/2/2006", pkg.InstalledOn)
	if err != nil {
		clog.Warningf(ctx, "Error parsing QFE InstalledOn date: %v", err)
	}

	return &agentendpointpb.Inventory_SoftwarePackage_QfePackage{
		QfePackage: &agentendpointpb.Inventory_WindowsQuickFixEngineeringPackage{
			Caption:     pkg.Caption,
			Description: pkg.Description,
			HotFixId:    pkg.HotFixID,
			InstallTime: timestamppb.New(installedTime),
		}}
}

func formatWindowsApplication(pkg *packages.WindowsApplication) *agentendpointpb.Inventory_SoftwarePackage_WindowsApplication {

	d := datepb.Date{}
	// We have to check if date is zero.
	// Because zero value of time has Year, Month, Day equal to 1
	if !pkg.InstallDate.IsZero() {
		d = datepb.Date{
			Year:  int32(pkg.InstallDate.Year()),
			Month: int32(pkg.InstallDate.Month()),
			Day:   int32(pkg.InstallDate.Day()),
		}
	}
	return &agentendpointpb.Inventory_SoftwarePackage_WindowsApplication{
		WindowsApplication: &agentendpointpb.Inventory_WindowsApplication{
			DisplayName:    pkg.DisplayName,
			DisplayVersion: pkg.DisplayVersion,
			Publisher:      pkg.Publisher,
			InstallDate:    &d,
			HelpLink:       pkg.HelpLink,
		}}
}

func computeFingerprint(ctx context.Context, inventory *agentendpointpb.Inventory) (string, error) {
	fingerprint := sha256.New()
	b, err := proto.Marshal(inventory)
	if err != nil {
		return "", err
	}
	io.Copy(fingerprint, bytes.NewReader(b))

	return hex.EncodeToString(fingerprint.Sum(nil)), nil
}

func computeStableFingerprint(ctx context.Context, inventory *agentendpointpb.Inventory) (string, error) {
	fingerprint := sha256.New()
	b, err := proto.Marshal(inventory.GetOsInfo())
	if err != nil {
		return "", err
	}
	io.Copy(fingerprint, bytes.NewReader(b))

	installedPackages := inventory.GetInstalledPackages()
	availablePackages := inventory.GetAvailablePackages()

	entries := make([]string, 0, len(installedPackages)+len(availablePackages))

	for _, pkg := range installedPackages {
		entries = append(entries, fingerprintForPackage(pkg))
	}

	for _, pkg := range availablePackages {
		entries = append(entries, fingerprintForPackage(pkg))
	}

	sort.Strings(entries)

	for _, entry := range entries {
		if _, err := io.WriteString(fingerprint, entry); err != nil {
			return "", err
		}
	}

	return hex.EncodeToString(fingerprint.Sum(nil)), nil
}

func fingerprintForPackage(pkg *agentendpointpb.Inventory_SoftwarePackage) string {
	//Inventory_WindowsUpdatePackage struct contains repeated fields
	//we should rely on fields that are stable and sufficient enough to uniquely identify the package.
	if wua := pkg.GetWuaPackage(); wua != nil {
		return fmt.Sprintf("%s-%s-%d", wua.GetTitle(), wua.GetUpdateId(), wua.GetRevisionNumber())
	}

	// For all packages other then wua we can just rely on proto String() method.
	return pkg.String()
}
