/*
Copyright 2022 Google LLC

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

    https://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 system contains types and functions needed to perform SAP System discovery operations.
package system

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"strings"
	"sync"
	"time"

	"github.com/cenkalti/backoff/v4"
	"cloud.google.com/go/logging"
	"golang.org/x/exp/slices"
	"google.golang.org/protobuf/encoding/protojson"
	"github.com/GoogleCloudPlatform/sapagent/internal/configuration"
	"github.com/GoogleCloudPlatform/sapagent/internal/system/appsdiscovery"
	"github.com/GoogleCloudPlatform/sapagent/internal/system/clouddiscovery"
	"github.com/GoogleCloudPlatform/sapagent/internal/system/hostdiscovery"
	"github.com/GoogleCloudPlatform/sapagent/internal/usagemetrics"
	"github.com/GoogleCloudPlatform/sapagent/internal/workloadmanager"
	"github.com/GoogleCloudPlatform/workloadagentplatform/sharedlibraries/log"
	"github.com/GoogleCloudPlatform/workloadagentplatform/sharedlibraries/recovery"

	timestamppb "google.golang.org/protobuf/types/known/timestamppb"
	cpb "github.com/GoogleCloudPlatform/sapagent/protos/configuration"
	ipb "github.com/GoogleCloudPlatform/sapagent/protos/instanceinfo"
	sappb "github.com/GoogleCloudPlatform/sapagent/protos/sapapp"
	dwpb "github.com/GoogleCloudPlatform/workloadagentplatform/sharedprotos/datawarehouse"
	spb "github.com/GoogleCloudPlatform/workloadagentplatform/sharedprotos/system"
)

const systemDiscoveryOverride = "/etc/google-cloud-sap-agent/system.json"

// SapSystemDiscoveryInterface is an interface for retrieving the list of SAP systems.
type SapSystemDiscoveryInterface interface {
	GetSAPSystems() []*spb.SapDiscovery
}

// SapAppDiscoveryInterface is an interface for retrieving the list of SAP instances.
type SapAppDiscoveryInterface interface {
	GetSAPInstances() *sappb.SAPInstances
}

// Discovery is a type used to perform SAP System discovery operations.
type Discovery struct {
	WlmService              WlmInterface
	CloudLogInterface       CloudLogInterface
	CloudDiscoveryInterface CloudDiscoveryInterface
	HostDiscoveryInterface  HostDiscoveryInterface
	SapDiscoveryInterface   SapDiscoveryInterface
	AppsDiscovery           func(context.Context, SapSystemDiscoveryInterface) *sappb.SAPInstances
	OSStatReader            workloadmanager.OSStatReader
	FileReader              workloadmanager.ConfigFileReader
	systems                 []*spb.SapDiscovery
	systemMu                sync.Mutex
	sapInstances            *sappb.SAPInstances
	sapMu                   sync.Mutex
	sapInstancesRoutine     *recovery.RecoverableRoutine
	systemDiscoveryRoutine  *recovery.RecoverableRoutine
}

// GetSAPSystems returns the current list of SAP Systems discovered on the current host.
func (d *Discovery) GetSAPSystems() []*spb.SapDiscovery {
	d.systemMu.Lock()
	defer d.systemMu.Unlock()
	return d.systems
}

// GetSAPInstances returns the current list of SAP Instances discovered on the current host.
func (d *Discovery) GetSAPInstances() *sappb.SAPInstances {
	d.sapMu.Lock()
	defer d.sapMu.Unlock()
	return d.sapInstances
}

// StartSAPSystemDiscovery Initializes the discovery object and starts the discovery subroutine.
// Returns true if the discovery goroutine is started, and false otherwise.
func StartSAPSystemDiscovery(ctx context.Context, config *cpb.Configuration, d *Discovery) bool {

	d.sapInstancesRoutine = &recovery.RecoverableRoutine{
		Routine:             updateSAPInstances,
		RoutineArg:          updateSapInstancesArgs{config, d},
		ErrorCode:           usagemetrics.DiscoverSapInstanceFailure,
		UsageLogger:         *usagemetrics.Logger,
		ExpectedMinDuration: 5 * time.Second,
	}
	d.sapInstancesRoutine.StartRoutine(ctx)

	// Ensure SAP instances is populated before starting system discovery
	backoff.Retry(func() error {
		if d.GetSAPInstances() != nil {
			return nil
		}
		return fmt.Errorf("SAP Instances not ready yet")
	}, backoff.WithMaxRetries(backoff.NewConstantBackOff(time.Second), 120))

	d.systemDiscoveryRoutine = &recovery.RecoverableRoutine{
		Routine:             runDiscovery,
		RoutineArg:          runDiscoveryArgs{config, d},
		ErrorCode:           usagemetrics.DiscoverSapSystemFailure,
		UsageLogger:         *usagemetrics.Logger,
		ExpectedMinDuration: 10 * time.Second,
	}

	d.systemDiscoveryRoutine.StartRoutine(ctx)

	// Ensure systems are populated before returning
	backoff.Retry(func() error {
		if d.GetSAPSystems() != nil {
			return nil
		}
		return fmt.Errorf("SAP Systems not ready yet")
	}, backoff.WithMaxRetries(backoff.NewConstantBackOff(time.Second), 120))
	return true
}

// CloudLogInterface is exported to be used by the system discovery OTE.
type CloudLogInterface interface {
	Log(e logging.Entry)
	Flush() error
}

// WlmInterface is exported to be used by the system discovery OTE.
type WlmInterface interface {
	WriteInsight(project, location string, writeInsightRequest *dwpb.WriteInsightRequest) error
}

// CloudDiscoveryInterface is exported to be used by the system discovery OTE.
type CloudDiscoveryInterface interface {
	DiscoverComputeResources(context.Context, *spb.SapDiscovery_Resource, string, []string, *ipb.CloudProperties) []*spb.SapDiscovery_Resource
}

// HostDiscoveryInterface is exported to be used by the system discovery OTE.
type HostDiscoveryInterface interface {
	DiscoverCurrentHost(context.Context) hostdiscovery.HostData
}

// SapDiscoveryInterface is exported to be used by the system discovery OTE.
type SapDiscoveryInterface interface {
	DiscoverSAPApps(ctx context.Context, sapApps *sappb.SAPInstances, conf *cpb.DiscoveryConfiguration) []appsdiscovery.SapSystemDetails
}

type loadBalancerGroup struct {
	lbRes        *spb.SapDiscovery_Resource
	instanceURIs []string
}

func removeDuplicates(res []*spb.SapDiscovery_Resource) []*spb.SapDiscovery_Resource {
	var out []*spb.SapDiscovery_Resource
	uris := make(map[string]*spb.SapDiscovery_Resource)
	for _, r := range res {
		outRes, ok := uris[r.ResourceUri]
		if !ok {
			uris[r.ResourceUri] = r
			out = append(out, r)
		} else {
			for _, rel := range r.RelatedResources {
				if !slices.Contains(outRes.RelatedResources, rel) {
					outRes.RelatedResources = append(outRes.RelatedResources, rel)
				}
			}
			if r.GetInstanceProperties() != nil {
				log.Logger.Debugw("Stored instance properties", "properties", outRes.GetInstanceProperties().String())
				log.Logger.Debugw("Duplicate instance properties", "properties", r.InstanceProperties.String())
				if outRes.InstanceProperties == nil {
					outRes.InstanceProperties = r.InstanceProperties
				} else {
					outRes.InstanceProperties.InstanceRole |= r.InstanceProperties.InstanceRole
					if r.InstanceProperties.GetVirtualHostname() != "" {
						outRes.InstanceProperties.VirtualHostname = r.InstanceProperties.VirtualHostname
					}
					apps := make(map[string]string, len(outRes.InstanceProperties.AppInstances))
					for _, app := range outRes.InstanceProperties.AppInstances {
						apps[app.Name] = app.Number
						log.Logger.Debugw("App instance", "app", app.String())
					}
					for _, app := range r.InstanceProperties.AppInstances {
						if _, o := apps[app.Name]; !o {
							outRes.InstanceProperties.AppInstances = append(outRes.InstanceProperties.AppInstances, app)
							apps[app.Name] = app.Number
							log.Logger.Debugw("Adding app instance", "app", app.String())
						} else {
							log.Logger.Debugw("Duplicate app instance", "app", app.String())
						}
					}
				}
				log.Logger.Debugw("Merged properties", "properties", outRes.InstanceProperties.String())
			}
		}
	}
	return out
}

type updateSapInstancesArgs struct {
	config *cpb.Configuration
	d      *Discovery
}

type runDiscoveryArgs struct {
	config *cpb.Configuration
	d      *Discovery
}

func updateSAPInstances(ctx context.Context, a any) {
	var args updateSapInstancesArgs
	var ok bool
	if args, ok = a.(updateSapInstancesArgs); !ok {
		log.CtxLogger(ctx).Warn("args is not of type updateSapInstancesArgs")
		return
	}

	if fileInfo, err := args.d.OSStatReader(systemDiscoveryOverride); fileInfo != nil && err == nil {
		args.d.sapInstances = &sappb.SAPInstances{}
		return
	}
	log.CtxLogger(ctx).Info("Starting SAP Instances update")
	updateTicker := time.NewTicker(args.config.GetDiscoveryConfiguration().GetSapInstancesUpdateFrequency().AsDuration())
	for {
		log.CtxLogger(ctx).Info("Updating SAP Instances")
		sapInst := args.d.AppsDiscovery(ctx, args.d)
		args.d.sapMu.Lock()
		args.d.sapInstances = sapInst
		args.d.sapMu.Unlock()

		select {
		case <-ctx.Done():
			log.CtxLogger(ctx).Info("SAP Discovery cancellation requested")
			return
		case <-updateTicker.C:
			continue
		}
	}
}

func runDiscovery(ctx context.Context, a any) {
	log.CtxLogger(ctx).Info("Starting SAP System Discovery")
	var args runDiscoveryArgs
	var ok bool
	if args, ok = a.(runDiscoveryArgs); !ok {
		log.CtxLogger(ctx).Warn("args is not of type runDiscoveryArgs")
		return
	}
	cp := args.config.GetCloudProperties()
	if cp == nil {
		log.CtxLogger(ctx).Warn("No Metadata Cloud Properties found, cannot collect resource information from the Compute API")
		return
	}

	updateTicker := time.NewTicker(args.config.GetDiscoveryConfiguration().GetSystemDiscoveryUpdateFrequency().AsDuration())
	for {
		sapSystems := args.d.discoverSAPSystems(ctx, cp, args.config)
		log.CtxLogger(ctx).Debugw("Discovered SAP Systems", "systems", sapSystems)

		locationParts := strings.Split(cp.GetZone(), "-")
		region := strings.Join([]string{locationParts[0], locationParts[1]}, "-")

		// Write SAP system discovery data only if sap_system_discovery is enabled.
		if args.config.GetDiscoveryConfiguration().GetEnableDiscovery().GetValue() {
			log.CtxLogger(ctx).Info("Sending systems to WLM API")
			for _, sys := range sapSystems {
				sys.ProjectNumber = cp.GetNumericProjectId()
				sys.UpdateTime = timestamppb.Now()
				// Remove fields only used for local discovery:
				// resource.instanceProperties.diskDeviceNames
				var comps []*spb.SapDiscovery_Component
				if sys.ApplicationLayer != nil {
					comps = append(comps, sys.ApplicationLayer)
				}
				if sys.DatabaseLayer != nil {
					comps = append(comps, sys.DatabaseLayer)
				}
				for len(comps) > 0 {
					comp := comps[len(comps)-1]
					comps = comps[:len(comps)-1]
					if comp.ReplicationSites != nil {
						for _, site := range comp.ReplicationSites {
							comps = append(comps, site.Component)
						}
					}
					for _, res := range comp.GetResources() {
						if res.GetInstanceProperties() != nil {
							res.InstanceProperties.DiskDeviceNames = nil
						}
					}
				}
				log.CtxLogger(ctx).Debugw("System to send to WLM", "system", sys)
				// Send System to DW API
				insightRequest := &dwpb.WriteInsightRequest{
					Insight: &dwpb.Insight{
						SapDiscovery: sys,
						InstanceId:   cp.GetInstanceId(),
					},
				}
				insightRequest.AgentVersion = configuration.AgentVersion

				err := args.d.WlmService.WriteInsight(cp.ProjectId, region, insightRequest)
				if err != nil {
					log.CtxLogger(ctx).Infow("Encountered error writing to WLM", "error", err)
				}

				if args.d.CloudLogInterface == nil {
					continue
				}
				err = args.d.writeToCloudLogging(sys)
				if err != nil {
					log.CtxLogger(ctx).Infow("Encountered error writing to cloud logging", "error", err)
				}
			}
		}

		log.CtxLogger(ctx).Info("Done SAP System Discovery")

		args.d.systemMu.Lock()
		args.d.systems = sapSystems
		args.d.systemMu.Unlock()

		select {
		case <-ctx.Done():
			log.CtxLogger(ctx).Info("SAP Discovery cancellation requested")
			return
		case <-updateTicker.C:
			continue
		}
	}
}

func (d *Discovery) discoverOverrideSystem(ctx context.Context, overrideFile string, instanceResource *spb.SapDiscovery_Resource) []*spb.SapDiscovery {
	file, err := d.FileReader(overrideFile)
	if err != nil {
		log.CtxLogger(ctx).Warnf("Failed to open override file: %v", err)
		return nil
	}
	defer file.Close()
	var fileBytes []byte
	if fileBytes, err = ioutil.ReadAll(file); err != nil {
		log.CtxLogger(ctx).Warnf("Failed to read override file: %v", err)
		return nil
	}
	log.CtxLogger(ctx).Debugf("File bytes: %s", fileBytes)
	var system spb.SapDiscovery
	if err := protojson.Unmarshal(fileBytes, &system); err != nil {
		log.CtxLogger(ctx).Warnf("Failed to decode override file: %v", err)
		return nil
	}
	// Make the instances without URI refer to this one.
	for _, r := range system.GetApplicationLayer().GetResources() {
		if r.GetResourceKind() == spb.SapDiscovery_Resource_RESOURCE_KIND_INSTANCE &&
			r.GetResourceUri() == "" {
			r.ResourceUri = instanceResource.GetResourceUri()
			r.InstanceProperties = instanceResource.InstanceProperties
		}
	}
	for _, r := range system.GetDatabaseLayer().GetResources() {
		if r.GetResourceKind() == spb.SapDiscovery_Resource_RESOURCE_KIND_INSTANCE &&
			r.GetResourceUri() == "" {
			r.ResourceUri = instanceResource.GetResourceUri()
			r.InstanceProperties = instanceResource.InstanceProperties
		}
	}
	return []*spb.SapDiscovery{&system}
}

func (d *Discovery) discoverSAPSystems(ctx context.Context, cp *ipb.CloudProperties, config *cpb.Configuration) []*spb.SapDiscovery {
	instanceURI := fmt.Sprintf("projects/%s/zones/%s/instances/%s", cp.GetProjectId(), cp.GetZone(), cp.GetInstanceName())
	log.CtxLogger(ctx).Debug("Discovering current host")
	hostInstanceResources := d.CloudDiscoveryInterface.DiscoverComputeResources(ctx, nil, "", []string{instanceURI}, cp)
	log.CtxLogger(ctx).Debugw("Host Resources", "hostResources", hostInstanceResources)
	var instanceResource *spb.SapDiscovery_Resource
	var instanceNetwork string
	// Find the instance resource and its subnetwork
	for _, r := range hostInstanceResources {
		if r.GetResourceKind() == spb.SapDiscovery_Resource_RESOURCE_KIND_INSTANCE && strings.Contains(r.ResourceUri, cp.GetInstanceName()) {
			log.CtxLogger(ctx).Debugf("Instance Resource: %v", r)
			instanceResource = r
		}
		if r.GetResourceKind() == spb.SapDiscovery_Resource_RESOURCE_KIND_NETWORK {
			instanceNetwork = r.ResourceUri
		}
		if instanceResource != nil && instanceNetwork != "" {
			break
		}
	}
	if instanceResource == nil {
		log.CtxLogger(ctx).Debug("No instance resource found")
	}
	if fileInfo, err := d.OSStatReader(systemDiscoveryOverride); fileInfo != nil && err == nil {
		log.CtxLogger(ctx).Info("Discovering system from override file")
		return d.discoverOverrideSystem(ctx, systemDiscoveryOverride, instanceResource)
	}

	log.CtxLogger(ctx).Info("Starting host discovery")
	hostData := d.HostDiscoveryInterface.DiscoverCurrentHost(ctx)
	log.CtxLogger(ctx).Debugw("Host data", "names", hostData)
	if hostData.KernelVersion != nil && instanceResource != nil {
		log.CtxLogger(ctx).Debugw("Kernel version", "version", hostData.KernelVersion)
		instanceResource.InstanceProperties.OsKernelVersion = hostData.KernelVersion
	}
	log.CtxLogger(ctx).Infow("Discovering other host resources")
	hostResources := d.CloudDiscoveryInterface.DiscoverComputeResources(ctx, instanceResource, instanceNetwork, append(hostData.ClusterAddrs, hostData.NFSAddrs...), cp)
	hostResources = removeDuplicates(append(hostResources, hostInstanceResources...))
	log.CtxLogger(ctx).Debugw("Host Resources", "hostResources", hostResources)

	sapSystems := []*spb.SapDiscovery{}

	log.CtxLogger(ctx).Info("Starting SAP Discovery")
	sapDetails := d.SapDiscoveryInterface.DiscoverSAPApps(ctx, d.GetSAPInstances(), config.GetDiscoveryConfiguration())
	log.CtxLogger(ctx).Debugw("SAP Details", "details", sapDetails)
	for _, s := range sapDetails {
		system := &spb.SapDiscovery{}
		if s.AppComponent != nil {
			log.CtxLogger(ctx).Info("Discovering cloud resources for app")
			appRes := d.CloudDiscoveryInterface.DiscoverComputeResources(ctx, instanceResource, instanceNetwork, s.AppHosts, cp)
			log.CtxLogger(ctx).Debugf("App Resources: %v", appRes)
			if s.AppOnHost {
				appRes = append(appRes, hostResources...)
				log.CtxLogger(ctx).Debugf("App On Host Resources: %v", appRes)
			}
			if s.AppComponent.GetApplicationProperties().GetNfsUri() != "" {
				log.CtxLogger(ctx).Info("Discovering cloud resources for app NFS")
				nfsRes := d.CloudDiscoveryInterface.DiscoverComputeResources(ctx, instanceResource, instanceNetwork, []string{s.AppComponent.GetApplicationProperties().GetNfsUri()}, cp)
				if len(nfsRes) > 0 {
					appRes = append(appRes, nfsRes...)
					s.AppComponent.GetApplicationProperties().NfsUri = nfsRes[0].GetResourceUri()
				}
			}
			if s.AppComponent.GetApplicationProperties().GetAscsUri() != "" {
				log.CtxLogger(ctx).Info("Discovering cloud resources for app ASCS")
				ascsRes := d.CloudDiscoveryInterface.DiscoverComputeResources(ctx, instanceResource, instanceNetwork, []string{s.AppComponent.GetApplicationProperties().GetAscsUri()}, cp)
				if len(ascsRes) > 0 {
					log.CtxLogger(ctx).Debugw("ASCS Resources", "res", ascsRes)
					appRes = append(appRes, ascsRes...)
					s.AppComponent.GetApplicationProperties().AscsUri = ascsRes[0].GetResourceUri()
				}
			}
			if len(s.AppComponent.GetHaHosts()) > 0 {
				haRes := d.CloudDiscoveryInterface.DiscoverComputeResources(ctx, instanceResource, instanceNetwork, s.AppComponent.GetHaHosts(), cp)
				// Find the instances
				var haURIs []string
				for _, res := range haRes {
					if res.GetResourceKind() == spb.SapDiscovery_Resource_RESOURCE_KIND_INSTANCE {
						haURIs = append(haURIs, res.GetResourceUri())
					}
				}
				appRes = append(appRes, haRes...)
				s.AppComponent.HaHosts = haURIs
			}
			s.AppComponent.HostProject = cp.GetNumericProjectId()
			s.AppComponent.Resources = removeDuplicates(appRes)
			system.ApplicationLayer = s.AppComponent
		}
		if s.DBComponent != nil {
			log.CtxLogger(ctx).Info("Discovering cloud resources for database")
			dbRes := d.CloudDiscoveryInterface.DiscoverComputeResources(ctx, instanceResource, instanceNetwork, s.DBHosts, cp)
			log.CtxLogger(ctx).Debugw("Database Resources", "res", dbRes)
			if s.DBOnHost {
				dbRes = removeDuplicates(append(dbRes, hostResources...))
			}

			if s.DBDiskMap != nil {
				log.CtxLogger(ctx).Debugw("DB Disk Map", "map", s.DBDiskMap)
				for mountPath, deviceNames := range s.DBDiskMap {
					for _, deviceName := range deviceNames {
						// instanceResource instance properties disk mounts is a list of the attached disks with
						// where name is the disk's source and mountPoint is the device name.
						mount := &spb.SapDiscovery_Resource_InstanceProperties_DiskMount{
							MountPoint: mountPath,
						}
						for _, disk := range instanceResource.InstanceProperties.DiskDeviceNames {
							if strings.Contains(deviceName, disk.GetDeviceName()) {
								log.CtxLogger(ctx).Debugw("Disk for device name", "disk", disk, "mountPath", mountPath, "deviceName", deviceName)
								mount.DiskNames = append(mount.DiskNames, disk.GetSource())
							}
						}
						if len(mount.DiskNames) > 0 {
							instanceResource.InstanceProperties.DiskMounts = append(instanceResource.InstanceProperties.DiskMounts, mount)
						}
					}
				}
			}
			// Make a resource map for quicker lookup
			resourcesByType := make(map[spb.SapDiscovery_Resource_ResourceKind][]*spb.SapDiscovery_Resource)
			resourceByURI := make(map[string]*spb.SapDiscovery_Resource)
			for _, r := range dbRes {
				resourcesByType[r.GetResourceKind()] = append(resourcesByType[r.GetResourceKind()], r)
				resourceByURI[r.GetResourceUri()] = r
			}
			log.CtxLogger(ctx).Debugw("Resources by type", "resources", resourcesByType)
			log.CtxLogger(ctx).Debugw("Resources by URI", "resources", resourceByURI)
			// Find the load balancer groups
			var lbGroups []loadBalancerGroup
			for _, lb := range resourcesByType[spb.SapDiscovery_Resource_RESOURCE_KIND_BACKEND_SERVICE] {
				log.CtxLogger(ctx).Debugw("LB", "lb", lb)
				lbGroup := loadBalancerGroup{lbRes: lb, instanceURIs: []string{}}
				for _, related := range lb.RelatedResources {
					rr := resourceByURI[related]
					if rr.GetResourceKind() == spb.SapDiscovery_Resource_RESOURCE_KIND_INSTANCE {
						log.CtxLogger(ctx).Debugw("LB Instance", "instance", rr)
						lbGroup.instanceURIs = append(lbGroup.instanceURIs, related)
					} else if rr.GetResourceKind() == spb.SapDiscovery_Resource_RESOURCE_KIND_INSTANCE_GROUP {
						log.CtxLogger(ctx).Debugw("LB Instance Group", "instanceGroup", rr)
						for _, igr := range rr.RelatedResources {
							igres := resourceByURI[igr]
							if igres.GetResourceKind() == spb.SapDiscovery_Resource_RESOURCE_KIND_INSTANCE {
								log.CtxLogger(ctx).Debugw("LB Instance in Group", "instance", igres)
								lbGroup.instanceURIs = append(lbGroup.instanceURIs, igr)
							}
						}
					}
				}
				log.CtxLogger(ctx).Debugw("LB Group", "group", lbGroup)
				lbGroups = append(lbGroups, lbGroup)
			}
			log.CtxLogger(ctx).Debugw("LB Groups", "groups", lbGroups)
			if s.DBComponent.GetDatabaseProperties().GetSharedNfsUri() != "" {
				log.CtxLogger(ctx).Debug("Discovering cloud resources for database NFS")
				nfsRes := d.CloudDiscoveryInterface.DiscoverComputeResources(ctx, instanceResource, instanceNetwork, []string{s.DBComponent.GetDatabaseProperties().GetSharedNfsUri()}, cp)
				if len(nfsRes) > 0 {
					dbRes = append(dbRes, nfsRes...)
					s.DBComponent.GetDatabaseProperties().SharedNfsUri = nfsRes[0].GetResourceUri()
				}
			}
			s.DBComponent.Resources = removeDuplicates(dbRes)
			if s.DBInstance != nil && s.DBInstance.HanaReplicationTree != nil {
				log.CtxLogger(ctx).Debug("Discovering cloud resources for database replication sites")
				primarySite := d.discoverReplicationSite(ctx, s.DBInstance.HanaReplicationTree, s.DBComponent.GetSid(), instanceResource, instanceNetwork, lbGroups, cp)
				primarySite.Component.Properties = s.DBComponent.Properties
				s.DBComponent = primarySite.Component

				// Find the site this instance belongs to.
				addHostResourcesforSite(ctx, s.DBComponent, dbRes, cp)
			}
			for _, r := range s.DBComponent.GetResources() {
				if r.GetResourceKind() == spb.SapDiscovery_Resource_RESOURCE_KIND_INSTANCE {
					if r.InstanceProperties == nil {
						r.InstanceProperties = &spb.SapDiscovery_Resource_InstanceProperties{}
					}
					r.InstanceProperties.InstanceRole |= spb.SapDiscovery_Resource_InstanceProperties_INSTANCE_ROLE_DATABASE
				}
			}

			log.CtxLogger(ctx).Debug("Done discovering DB")
			s.DBComponent.HostProject = cp.GetNumericProjectId()
			system.DatabaseLayer = s.DBComponent
		}
		if len(s.InstanceProperties) > 0 {
			for _, iProp := range s.InstanceProperties {
				log.CtxLogger(ctx).Debugw("Discovering instance properties", "props", iProp)
				res := d.CloudDiscoveryInterface.DiscoverComputeResources(ctx, instanceResource, instanceNetwork, []string{iProp.VirtualHostname}, cp)
				log.CtxLogger(ctx).Debugf("Discovered instance properties: %s", res)
				if res == nil {
					continue
				}
				for _, r := range res {
					if r.GetResourceKind() == spb.SapDiscovery_Resource_RESOURCE_KIND_INSTANCE {
						if r.InstanceProperties == nil {
							r.InstanceProperties = &spb.SapDiscovery_Resource_InstanceProperties{}
						}
						if iProp.VirtualHostname != "" {
							r.InstanceProperties.VirtualHostname = iProp.VirtualHostname
						}
						r.InstanceProperties.InstanceRole |= iProp.InstanceRole

						appInstances := make(map[string]*spb.SapDiscovery_Resource_InstanceProperties_AppInstance)
						for _, app := range r.InstanceProperties.AppInstances {
							appInstances[app.GetName()] = app
						}
						for _, app := range iProp.AppInstances {
							if _, ok := appInstances[app.GetName()]; !ok {
								r.InstanceProperties.AppInstances = append(r.InstanceProperties.AppInstances, app)
								appInstances[app.GetName()] = app
							}
						}
						log.CtxLogger(ctx).Debugw("Adding instance properties to resource", "resource_uri", r.ResourceUri, "instance_properties", r.InstanceProperties)
					}
				}
				if (iProp.InstanceRole & spb.SapDiscovery_Resource_InstanceProperties_INSTANCE_ROLE_DATABASE) != 0 {
					log.CtxLogger(ctx).Debug("Instance properties are for a database instance")
					if system.GetDatabaseLayer() != nil {
						system.DatabaseLayer.Resources = removeDuplicates(append(system.GetDatabaseLayer().Resources, res...))
					}
				}
				if (iProp.InstanceRole & spb.SapDiscovery_Resource_InstanceProperties_INSTANCE_ROLE_ASCS_ERS_APP_SERVER) != 0 {
					log.CtxLogger(ctx).Debug("Instance properties are for a application instance")
					if system.GetApplicationLayer() != nil {
						system.ApplicationLayer.Resources = removeDuplicates(append(system.GetApplicationLayer().Resources, res...))
					}
				}
			}
		}
		system.WorkloadProperties = s.WorkloadProperties
		system.ProjectNumber = cp.GetNumericProjectId()
		system.UpdateTime = timestamppb.Now()
		sapSystems = append(sapSystems, system)
	}
	log.CtxLogger(ctx).Debug("Done discovering systems")
	return sapSystems
}

func (d *Discovery) discoverReplicationSite(ctx context.Context, site *sappb.HANAReplicaSite, sid string, instanceResource *spb.SapDiscovery_Resource, instanceSubnetwork string, lbGroups []loadBalancerGroup, cp *ipb.CloudProperties) *spb.SapDiscovery_Component_ReplicationSite {
	siteRes := d.CloudDiscoveryInterface.DiscoverComputeResources(ctx, instanceResource, instanceSubnetwork, []string{site.Name}, cp)
	log.CtxLogger(ctx).Debugw("Site Resources", "site", site.Name, "resources", siteRes)
	repComp := &spb.SapDiscovery_Component{
		Resources: removeDuplicates(siteRes),
		Sid:       sid,
	}

	// Find the region of the instance that the site name corresponds to.
	for _, r := range repComp.Resources {
		if r.GetResourceKind() == spb.SapDiscovery_Resource_RESOURCE_KIND_INSTANCE {
			if r.InstanceProperties == nil {
				r.InstanceProperties = &spb.SapDiscovery_Resource_InstanceProperties{}
			}
			r.InstanceProperties.InstanceRole |= spb.SapDiscovery_Resource_InstanceProperties_INSTANCE_ROLE_DATABASE
			if r.GetInstanceProperties().GetVirtualHostname() == site.Name {
				instanceZone := clouddiscovery.ExtractFromURI(r.GetResourceUri(), "zones")
				regionParts := strings.Split(instanceZone, "-")
				instanceRegion := strings.Join([]string{regionParts[0], regionParts[1]}, "-")
				repComp.Region = instanceRegion
			}
		}
	}

	// Check if this site is part of a known load balancer group
	var lbGroup *loadBalancerGroup
resLoop:
	for _, r := range siteRes {
		if r.ResourceKind != spb.SapDiscovery_Resource_RESOURCE_KIND_INSTANCE {
			continue
		}
		for _, lb := range lbGroups {
			if slices.Contains(lb.instanceURIs, r.GetResourceUri()) {
				log.CtxLogger(ctx).Debugw("Site is part of a load balancer group", "site", site.Name, "lbGroup", lb.lbRes.GetResourceUri())
				lbGroup = &lb
				break resLoop
			}
		}
	}
	if lbGroup == nil {
		log.CtxLogger(ctx).Debugw("Site not part of existing LB group", "site", site.Name)
	}
	for _, childSite := range site.Targets {
		childRepSite := d.discoverReplicationSite(ctx, childSite, sid, instanceResource, instanceSubnetwork, lbGroups, cp)
		inLbGroup := false
		if lbGroup != nil {
			// Check if the child site is part of the same load balancer group
			for _, r := range childRepSite.Component.Resources {
				if r.ResourceKind != spb.SapDiscovery_Resource_RESOURCE_KIND_INSTANCE {
					continue
				}
				if slices.Contains(lbGroup.instanceURIs, r.GetResourceUri()) {
					log.CtxLogger(ctx).Debugw("Child site is part of the same LB group", "site", childSite.Name, "lbGroup", lbGroup.lbRes.GetResourceUri())
					inLbGroup = true
					// Child is part of the same load balancer group, therefore belongs in the same component.
					repComp.Resources = removeDuplicates(append(repComp.Resources, childRepSite.Component.Resources...))
					// Add the child site's replication sites to this component.
					repComp.ReplicationSites = append(repComp.ReplicationSites, childRepSite.Component.ReplicationSites...)
					repComp.HaHosts = append(repComp.HaHosts, lbGroup.instanceURIs...)
					break
				}
			}
		}
		if !inLbGroup {
			log.CtxLogger(ctx).Debugw("Child site not part of the same LB group", "site", childSite.Name)
			// Child is not part of the same load balancer group, therefore belongs in a separate component.
			childRepSite.SourceSite = site.Name

			repComp.ReplicationSites = append(repComp.ReplicationSites, childRepSite)
		}
	}
	log.CtxLogger(ctx).Debugw("Site", "site", site.Name, "component", repComp)
	return &spb.SapDiscovery_Component_ReplicationSite{
		Component: repComp,
	}
}

func (d *Discovery) writeToCloudLogging(sys *spb.SapDiscovery) error {
	s, err := protojson.Marshal(sys)
	if err != nil {
		return err
	}
	var buf bytes.Buffer
	json.Indent(&buf, s, "", "  ")

	payload := make(map[string]string)
	payload["type"] = "SapDiscovery"
	payload["discovery"] = buf.String()

	d.CloudLogInterface.Log(logging.Entry{
		Timestamp: time.Now(),
		Severity:  logging.Info,
		Payload:   payload,
	})

	return nil
}

func addHostResourcesforSite(ctx context.Context, site *spb.SapDiscovery_Component, hostResources []*spb.SapDiscovery_Resource, cp *ipb.CloudProperties) {
	for _, r := range site.Resources {
		instanceName := clouddiscovery.ExtractFromURI(r.GetResourceUri(), "instances")
		if instanceName == cp.GetInstanceName() {
			// Add the host resources to this site.
			log.CtxLogger(ctx).Debugw("Adding host resources to site", "resources", hostResources)
			site.Resources = removeDuplicates(append(site.Resources, hostResources...))
			return
		}
	}
	// Host is not in the site, check children
	for _, rep := range site.ReplicationSites {
		addHostResourcesforSite(ctx, rep.Component, hostResources, cp)
	}
}
