//go:build windows && lcow

// containerd-shim-lcow-v2 is a containerd shim implementation for Linux Containers on Windows (LCOW).
package main

import (
	"context"
	"errors"
	"fmt"
	"io"
	"os"

	"github.com/Microsoft/hcsshim/cmd/containerd-shim-lcow-v2/service"
	_ "github.com/Microsoft/hcsshim/cmd/containerd-shim-lcow-v2/service/plugin"
	runhcsopts "github.com/Microsoft/hcsshim/cmd/containerd-shim-runhcs-v1/options"
	"github.com/Microsoft/hcsshim/internal/log"
	"github.com/Microsoft/hcsshim/internal/ot"
	"github.com/Microsoft/hcsshim/internal/shim"
	"github.com/Microsoft/hcsshim/osversion"

	bootapi "github.com/containerd/containerd/api/runtime/bootstrap/v1"
	"github.com/sirupsen/logrus"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/propagation"
	sdktrace "go.opentelemetry.io/otel/sdk/trace"
	"google.golang.org/protobuf/proto"
)

// Add a manifest to get proper Windows version detection.
//go:generate pwsh -Command "../../scripts/New-ResourceObjectFile.ps1 -ErrorAction 'Stop' -Destination '.' -Name 'containerd-shim-lcow-v2' -UseVersionFile -Architecture 'all'"

func initOtelTracer() (func(context.Context) error, error) {
	exporter := &ot.LogrusExporter{}
	traceProvider := sdktrace.NewTracerProvider(
		sdktrace.WithSyncer(exporter),
		sdktrace.WithSampler(sdktrace.AlwaysSample()),
	)
	if traceProvider == nil {
		return nil, errors.New("failed to construct OpenTelemetry tracer provider")
	}
	otel.SetTracerProvider(traceProvider)
	if otel.GetTracerProvider() != traceProvider {
		return nil, errors.New("failed to register OpenTelemetry tracer provider globally")
	}
	otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
		propagation.TraceContext{},
		propagation.Baggage{},
	))
	return traceProvider.Shutdown, nil
}

func main() {
	logrus.AddHook(log.NewHook())

	// Register our OpenTelemetry logrus exporter so that trace spans are emitted via logrus.
	shutdownTracer, err := initOtelTracer()
	if err != nil {
		logrus.Fatalf("failed to initialize ot tracer: %v", err)
	}
	defer func() { _ = shutdownTracer(context.Background()) }()

	logrus.SetFormatter(log.NopFormatter{})
	logrus.SetOutput(io.Discard)

	// Set the log configuration.
	// If we encounter an error, we exit with non-zero code.
	if err := setLogConfiguration(); err != nil {
		_, _ = fmt.Fprintf(os.Stderr, "%s: %s", service.ShimName, err)
		os.Exit(1)
	}

	// This shim is supported on Windows Build 26100 and later.
	if osversion.Build() < osversion.V25H1Server {
		_, _ = fmt.Fprintf(os.Stderr,
			"%s: Windows version [%v] is not supported", service.ShimName, osversion.Build())
		os.Exit(1)
	}

	// Start the shim manager event loop. The manager is responsible for
	// handling containerd start/stop lifecycle calls for the shim process.
	shim.Run(context.Background(), newShimManager(service.ShimName), func(c *shim.Config) {
		// We don't want the shim package to set up logging options.
		c.NoSetupLogger = true
	})
}

// setLogConfiguration reads the runtime options from stdin and sets the log configuration.
// We only set up the log configuration for serve action.
func setLogConfiguration() error {
	// We set up the log configuration in the serve action only.
	// This is because we want to avoid reading the stdin in start action,
	// so that we can pass it along to the invocation for serve action.
	if len(os.Args) > 1 && os.Args[len(os.Args)-1] == "serve" {
		// The serve process is started with stderr pointing to panic.log file.
		// We want to keep that file only for pure Go panics. Any explicit writes
		// to os.Stderr should go to stdout instead, which is connected to the parent's
		// stderr for regular logging.
		// We can safely redirect os.Stderr to os.Stdout because in case of panics,
		// the Go runtime will write the panic stack trace directly to the file descriptor,
		// bypassing os.Stderr, so it will still go to panic.log.
		os.Stderr = os.Stdout

		data, err := io.ReadAll(os.Stdin)
		if err != nil {
			return fmt.Errorf("failed to read runtime options from stdin: %w", err)
		}
		_ = os.Stdin.Close()

		// containerd 2.3+ delivers the options as an extension inside BootstrapParams.
		var params bootapi.BootstrapParams
		if err := proto.Unmarshal(data, &params); err != nil {
			return fmt.Errorf("failed to unmarshal bootstrap params: %w", err)
		}
		var opts runhcsopts.Options
		found, err := params.FindExtension(&opts)
		if err != nil {
			return fmt.Errorf("failed to extract runtime options from bootstrap params: %w", err)
		}
		if found {
			if opts.LogLevel != "" {
				// If log level is specified, set the corresponding logrus logging level.
				lvl, err := logrus.ParseLevel(opts.LogLevel)
				if err != nil {
					return fmt.Errorf("failed to parse shim log level %q: %w", opts.LogLevel, err)
				}
				logrus.SetLevel(lvl)
			}

			// Scrubbing is enabled by default (via init() in internal/log/scrub.go).
			// Only disable if the option is explicitly set to false.
			if opts.ScrubLogs != nil && !*opts.ScrubLogs {
				log.SetScrubbing(false)
			}
		}
	}
	return nil
}
