//  Copyright 2024 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
//
//      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 logger wraps the galog configuration/initialization.
package logger

import (
	"context"
	"fmt"
	"os"
	"path/filepath"
	"regexp"
	"time"

	"github.com/GoogleCloudPlatform/agentcommunication_client"
	"github.com/GoogleCloudPlatform/galog"
	"github.com/GoogleCloudPlatform/google-guest-agent/internal/events"
	"github.com/GoogleCloudPlatform/google-guest-agent/internal/metadata"
	"github.com/GoogleCloudPlatform/google-guest-agent/internal/utils/file"
)

// Options contains the loggers configuration/options.
type Options struct {
	// CloudIdent is the cloud logging logId attribute - or logName field.
	CloudIdent string
	// Ident is the application ident used across loggers.
	Ident string
	// ProgramVersion is the program version.
	ProgramVersion string
	// LogFile is the path of the log file.
	LogFile string
	// LogToStderr flags if stderr loggers must be enabled.
	LogToStderr bool
	// LogToCloudLogging flags if cloud logging loggers must be enabled.
	LogToCloudLogging bool
	// cloudLoggingBackend is the cloud logging backend.
	cloudLoggingBackend *galog.CloudBackend
	// cloudLoggingWithoutAuthentication flags if cloud logging should be
	// initialized without authentication. Useful for testing.
	cloudLoggingWithoutAuthentication bool
	// Level is the log level.
	Level int
	// Verbosity is the log verbosity level.
	Verbosity int
	// Prefix is a prefix tag appended to all log entries, it's passed down to
	// galog configuration.
	Prefix string
	// ACSClientDebugLogging is a flag to enable ACS client logging.
	ACSClientDebugLogging bool
	// InitCloudLoggingImmediately is a flag to initialize cloud logging
	// immediately.
	InitCloudLoggingImmediately bool
}

const (
	// loggerMetadataSubscriberID is the subscriber ID for the logger metadata
	// event used to register the cloud logging backend - it can only be
	// registered after we got the first metadata event since cloud logging
	// requires at least the project name to be registered.
	loggerMetadataSubscriberID = "logger_metadata_subscriber"

	// cloudLoggingFlushCadence is the cadence of flushing cloud logging data.
	cloudLoggingFlushCadence = time.Second * 5

	// CloudLoggingLogID is the logId used for cloud logging for core plugin.
	CloudLoggingLogID = "GCEGuestAgent"

	// LocalLoggerIdent is the ident used for local loggers (i.e syslog), it is
	// shared between core plugin and guest agent - they both use the same
	// "name space".
	LocalLoggerIdent = "google_guest_agent"

	// CorePluginLogPrefix is a human readable prefix added to all log entries
	// identifying the core plugin.
	CorePluginLogPrefix = "CorePlugin"

	// ManagerCloudLoggingLogID is the logId used for cloud logging for plugin
	// manager.
	ManagerCloudLoggingLogID = "GCEGuestAgentManager"
	// ManagerLocalLoggerIdent is the ident used for local loggers (i.e syslog)
	// for plugin manager.
	ManagerLocalLoggerIdent = "google_guest_agent_manager"
	// ManagerLogPrefix is a human readable prefix added to all log entries for
	// plugin manager.
	ManagerLogPrefix = "GCEGuestAgentManager"

	// The following are MIG labels added to the cloud logging logs.
	// MIGNameLabel is the MIG name label.
	migNameLabel = `compute.googleapis.com/instance_group_manager/name`
	// migZoneLabel is the MIG zone label.
	migZoneLabel = `compute.googleapis.com/instance_group_manager/zone`
	// migRegionLabel is the MIG region label.
	migRegionLabel = `compute.googleapis.com/instance_group_manager/region`
)

// Init initializes the logger.
func Init(ctx context.Context, opts Options) error {
	enabledLoggers, err := initPlatformLogger(ctx, opts.Ident, opts.Prefix)
	if err != nil {
		return fmt.Errorf("failed to initialize platform logger: %w", err)
	}

	galog.SetMinVerbosity(opts.Verbosity)

	if opts.LogFile != "" && file.Exists(filepath.Dir(opts.LogFile), file.TypeDir) {
		galog.V(2).Debugf("Initializing file logger: %s", opts.LogFile)
		enabledLoggers = append(enabledLoggers, galog.NewFileBackend(opts.LogFile))
	}

	if opts.LogToStderr {
		galog.V(2).Debugf("Initializing stderr logger")
		enabledLoggers = append(enabledLoggers, galog.NewStderrBackend())
	}

	for _, logger := range enabledLoggers {
		galog.RegisterBackend(ctx, logger)
	}

	if opts.LogToCloudLogging {
		galog.V(2).Debugf("Initializing cloud logging backend")

		be, err := galog.NewCloudBackend(ctx, galog.CloudLoggingInitModeLazy, nil)
		galog.RegisterBackend(ctx, be)
		if err != nil {
			return fmt.Errorf("failed to initialize cloud logging: %w", err)
		}
		opts.cloudLoggingBackend = be

		if opts.InitCloudLoggingImmediately {
			mds, err := metadata.New().Get(ctx)
			if err != nil {
				return fmt.Errorf("failed to get metadata descriptor: %w", err)
			}
			_, _, err = initCloudLogging(ctx, "", &opts, &events.EventData{Data: mds})
			if err != nil {
				return fmt.Errorf("failed to initialize cloud logging: %w", err)
			}
		} else {
			// We initialize and register the cloud logging backend in a lazy mode,
			// meaning the cloud logging client will only be initialized when the
			// metadata longpoll event is handled by initCloudLogging() subscriber.
			sub := events.EventSubscriber{Name: loggerMetadataSubscriberID, Data: &opts, Callback: initCloudLogging}
			events.FetchManager().Subscribe(metadata.LongpollEvent, sub)
		}
	}

	level, err := galog.ParseLevel(opts.Level)
	if err != nil {
		return fmt.Errorf("invalid log level: %w", err)
	}

	galog.SetLevel(level)
	client.DebugLogging = opts.ACSClientDebugLogging

	return nil
}

// parseMIGLabels parses the MIG labels from the created-by metadata attribute.
// This is used to add extra labels to the Cloud Logging logs.
func parseMIGLabels(mds *metadata.Descriptor) (map[string]string, error) {
	labels := make(map[string]string)
	createdBy := mds.Instance().Attributes().CreatedBy()
	if createdBy == "" {
		return labels, nil
	}

	// Make sure the `created-by` is set by MIG.
	migRe, err := regexp.Compile(`^projects/[^/]+/(zones|regions)/([^/]+)/instanceGroupManagers/([^/]+)$`)
	if err != nil {
		return labels, err
	}
	migMatch := migRe.FindStringSubmatch(mds.Instance().Attributes().CreatedBy())
	if migMatch == nil {
		return labels, nil
	}

	var locationLabel string
	switch migMatch[1] {
	case "zones":
		locationLabel = migZoneLabel
	case "regions":
		locationLabel = migRegionLabel
	}
	labels[migNameLabel] = migMatch[3]
	labels[locationLabel] = migMatch[2]
	return labels, nil
}

// initCloudLogging is a subscribed event handler to metadata event. Cloud
// Logging initialization depends on data provided by metadata server, we can
// only initialize if after having the first descriptor being available. This
// handler/subscriber will never be renewed.
func initCloudLogging(ctx context.Context, eventType string, data any, event *events.EventData) (bool, bool, error) {
	// Any invalid data or event data will be dealt as self correctable error
	// meaning we return true to indicate the event should be retried.
	opts, ok := data.(*Options)
	if !ok {
		return true, true, fmt.Errorf("failed to initialize cloud logging, invalid callback data type (%T) passed", data)
	}

	mds, ok := event.Data.(*metadata.Descriptor)
	if !ok {
		return true, true, fmt.Errorf("failed to initialize cloud logging, invalid event.Data type (%T) passed to event callback", event.Data)
	}

	extraLabels, err := parseMIGLabels(mds)
	if err != nil {
		galog.Errorf("Failed to parse MIG labels: %v", err)
	}

	programName := filepath.Base(os.Args[0])
	cloudOpts := &galog.CloudOptions{
		Ident: opts.CloudIdent,
		// Core plugin and guest agent use the same logId (here Ident), in a way
		// to differentiate log entries we add the ProgName to the log entry's
		// payload.
		ProgramName:           programName,
		ProgramVersion:        opts.ProgramVersion,
		Project:               mds.Project().ID(),
		FlushCadence:          cloudLoggingFlushCadence,
		Instance:              mds.Instance().Name(),
		WithoutAuthentication: opts.cloudLoggingWithoutAuthentication,
		ExtraLabels:           extraLabels,
		// If in GDU this will be empty, so it's no effect and is ignored by galog
		// and the cloud logging client library.
		UniverseDomain: mds.Universe().UniverseDomain(),
	}

	if err := opts.cloudLoggingBackend.InitClient(ctx, cloudOpts); err != nil {
		return true, false, fmt.Errorf("failed to initialize cloud logging client(%s): %w", programName, err)
	}

	galog.Infof("Cloud logging initialized (%s)", programName)
	return false, false, nil
}
