// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

// Offsetgen is a utility to generate a static file containing offsets for Go
// struct fields.
package main

import (
	"context"
	"encoding/json"
	"flag"
	"fmt"
	"log/slog"
	"os"
	"os/signal"
	"runtime"

	"go.opentelemetry.io/auto/internal/pkg/structfield"
	"go.opentelemetry.io/auto/internal/tools/inspect"
)

const (
	defaultOutputFile = "offset_results.json"

	minGoVersion = "1.19"
)

var (
	// outputFile is the output file path flag value.
	outputFile string
	// cacheFile is the offset cache file path flag value.
	cacheFile string
	// numCPU is the number of CPUs to use flag value.
	numCPU int
	// verbosity is the log verbosity level flag value.
	verbosity int

	logger *slog.Logger
)

func init() {
	flag.StringVar(&outputFile, "output", defaultOutputFile, "output file")
	flag.StringVar(&cacheFile, "cache", "", "offset cache")
	flag.IntVar(&numCPU, "workers", runtime.NumCPU(), "max number of Goroutine workers")
	flag.IntVar(&verbosity, "v", 0, "log verbosity")

	flag.Parse()

	logger = slog.Default()
}

func manifests() ([]inspect.Manifest, error) {
	goVers, err := GoVersions(">= " + minGoVersion)
	if err != nil {
		return nil, fmt.Errorf("failed to get Go versions: %w", err)
	}

	grpcVers, err := PkgVersions("google.golang.org/grpc")
	if err != nil {
		return nil, fmt.Errorf("failed to get \"google.golang.org/grpc\" versions: %w", err)
	}

	xNetVers, err := PkgVersions("golang.org/x/net")
	if err != nil {
		return nil, fmt.Errorf("failed to get \"golang.org/x/net\" versions: %w", err)
	}

	goOtelVers, err := PkgVersions("go.opentelemetry.io/otel")
	if err != nil {
		return nil, fmt.Errorf("failed to get \"go.opentelemetry.io/otel\" versions: %w", err)
	}

	kafkaGoVers, err := PkgVersions("github.com/segmentio/kafka-go")
	if err != nil {
		return nil, fmt.Errorf("failed to get \"github.com/segmentio/kafka-go\" versions: %w", err)
	}

	ren := func(src string) inspect.Renderer {
		return inspect.NewRenderer(logger, src, inspect.DefaultFS)
	}

	return []inspect.Manifest{
		{
			Application: inspect.Application{
				Renderer:  ren("templates/runtime/*.tmpl"),
				GoVerions: goVers,
			},
			StructFields: []structfield.ID{
				structfield.NewID("std", "runtime", "g", "goid"),
				structfield.NewID("std", "runtime", "hmap", "buckets"),
			},
		},
		{
			Application: inspect.Application{
				Renderer:  ren("templates/net/http/*.tmpl"),
				GoVerions: goVers,
			},
			StructFields: []structfield.ID{
				structfield.NewID("std", "net/http", "Request", "Method"),
				structfield.NewID("std", "net/http", "Request", "URL"),
				structfield.NewID("std", "net/http", "Request", "RemoteAddr"),
				structfield.NewID("std", "net/http", "Request", "Header"),
				structfield.NewID("std", "net/http", "Request", "ctx"),
				structfield.NewID("std", "net/http", "Response", "StatusCode"),
				structfield.NewID("std", "net/http", "response", "req"),
				structfield.NewID("std", "net/http", "response", "status"),
				structfield.NewID("std", "net/http", "Request", "Proto"),
				structfield.NewID("std", "net/http", "Request", "RequestURI"),
				structfield.NewID("std", "net/http", "Request", "Host"),
				structfield.NewID("std", "net/http", "Request", "Pattern"),
				structfield.NewID("std", "net/http", "Request", "pat"),
				structfield.NewID("std", "net/http", "pattern", "str"),
				structfield.NewID("std", "net/url", "URL", "Path"),
				structfield.NewID("std", "net/url", "URL", "Scheme"),
				structfield.NewID("std", "net/url", "URL", "Opaque"),
				structfield.NewID("std", "net/url", "URL", "User"),
				structfield.NewID("std", "net/url", "URL", "RawPath"),
				structfield.NewID("std", "net/url", "URL", "OmitHost"),
				structfield.NewID("std", "net/url", "URL", "ForceQuery"),
				structfield.NewID("std", "net/url", "URL", "RawQuery"),
				structfield.NewID("std", "net/url", "URL", "Fragment"),
				structfield.NewID("std", "net/url", "URL", "RawFragment"),
				structfield.NewID("std", "net/url", "URL", "Host"),
				structfield.NewID("std", "net/url", "Userinfo", "username"),
				structfield.NewID("std", "bufio", "Writer", "buf"),
				structfield.NewID("std", "bufio", "Writer", "n"),
				structfield.NewID("std", "net", "TCPAddr", "IP"),
				structfield.NewID("std", "net", "TCPAddr", "Port"),
			},
		},
		{
			Application: inspect.Application{
				Renderer: ren("templates/google.golang.org/grpc/*.tmpl"),
				Versions: grpcVers,
			},
			StructFields: []structfield.ID{
				structfield.NewID(
					"google.golang.org/grpc",
					"google.golang.org/grpc/internal/transport",
					"Stream",
					"method",
				),
				structfield.NewID(
					"google.golang.org/grpc",
					"google.golang.org/grpc/internal/transport",
					"Stream",
					"id",
				),
				structfield.NewID(
					"google.golang.org/grpc",
					"google.golang.org/grpc/internal/transport",
					"Stream",
					"ctx",
				),
				structfield.NewID(
					"google.golang.org/grpc",
					"google.golang.org/grpc/internal/transport",
					"ServerStream",
					"Stream",
				),
				structfield.NewID(
					"google.golang.org/grpc",
					"google.golang.org/grpc",
					"ClientConn",
					"target",
				),
				structfield.NewID(
					"google.golang.org/grpc",
					"google.golang.org/grpc/internal/transport",
					"http2Client",
					"nextID",
				),
				structfield.NewID(
					"google.golang.org/grpc",
					"google.golang.org/grpc/internal/transport",
					"headerFrame",
					"streamID",
				),
				structfield.NewID(
					"google.golang.org/grpc",
					"google.golang.org/grpc/internal/transport",
					"headerFrame",
					"hf",
				),
				structfield.NewID(
					"google.golang.org/grpc",
					"google.golang.org/grpc/internal/status",
					"Error",
					"s",
				),
				structfield.NewID(
					"google.golang.org/grpc",
					"google.golang.org/grpc/internal/status",
					"Status",
					"s",
				),
				structfield.NewID(
					"google.golang.org/grpc",
					"google.golang.org/genproto/googleapis/rpc/status",
					"Status",
					"Code",
				),
				structfield.NewID(
					"google.golang.org/grpc",
					"google.golang.org/genproto/googleapis/rpc/status",
					"Status",
					"Message",
				),
				structfield.NewID(
					"google.golang.org/grpc",
					"google.golang.org/grpc/internal/transport",
					"http2Server",
					"peer",
				),
				structfield.NewID(
					"google.golang.org/grpc",
					"google.golang.org/grpc/peer",
					"Peer",
					"LocalAddr",
				),
			},
		},
		{
			Application: inspect.Application{
				Renderer: ren("templates/golang.org/x/net/*.tmpl"),
				Versions: xNetVers,
			},
			StructFields: []structfield.ID{
				structfield.NewID(
					"golang.org/x/net",
					"golang.org/x/net/http2",
					"MetaHeadersFrame",
					"Fields",
				),
				structfield.NewID(
					"golang.org/x/net",
					"golang.org/x/net/http2",
					"FrameHeader",
					"StreamID",
				),
			},
		},
		{
			Application: inspect.Application{
				Renderer: ren("templates/go.opentelemetry.io/otel/traceglobal/*.tmpl"),
				Versions: goOtelVers,
			},
			StructFields: []structfield.ID{
				structfield.NewID(
					"go.opentelemetry.io/otel",
					"go.opentelemetry.io/otel/internal/global",
					"tracer",
					"delegate",
				),
				structfield.NewID(
					"go.opentelemetry.io/otel",
					"go.opentelemetry.io/otel/internal/global",
					"tracer",
					"name",
				),
				structfield.NewID(
					"go.opentelemetry.io/otel",
					"go.opentelemetry.io/otel/internal/global",
					"tracer",
					"provider",
				),
				structfield.NewID(
					"go.opentelemetry.io/otel",
					"go.opentelemetry.io/otel/internal/global",
					"tracerProvider",
					"tracers",
				),
				structfield.NewID(
					"go.opentelemetry.io/otel",
					"go.opentelemetry.io/otel/trace",
					"SpanContext",
					"traceID",
				),
				structfield.NewID(
					"go.opentelemetry.io/otel",
					"go.opentelemetry.io/otel/trace",
					"SpanContext",
					"spanID",
				),
				structfield.NewID(
					"go.opentelemetry.io/otel",
					"go.opentelemetry.io/otel/trace",
					"SpanContext",
					"traceFlags",
				),
			},
		},
		{
			Application: inspect.Application{
				Renderer: ren("templates/github.com/segmentio/kafka-go/*.tmpl"),
				Versions: kafkaGoVers,
			},
			StructFields: []structfield.ID{
				structfield.NewID(
					"github.com/segmentio/kafka-go",
					"github.com/segmentio/kafka-go",
					"Message",
					"Topic",
				),
				structfield.NewID(
					"github.com/segmentio/kafka-go",
					"github.com/segmentio/kafka-go",
					"Message",
					"Partition",
				),
				structfield.NewID(
					"github.com/segmentio/kafka-go",
					"github.com/segmentio/kafka-go",
					"Message",
					"Offset",
				),
				structfield.NewID(
					"github.com/segmentio/kafka-go",
					"github.com/segmentio/kafka-go",
					"Message",
					"Key",
				),
				structfield.NewID(
					"github.com/segmentio/kafka-go",
					"github.com/segmentio/kafka-go",
					"Message",
					"Headers",
				),
				structfield.NewID(
					"github.com/segmentio/kafka-go",
					"github.com/segmentio/kafka-go",
					"Message",
					"Time",
				),
				structfield.NewID(
					"github.com/segmentio/kafka-go",
					"github.com/segmentio/kafka-go",
					"Writer",
					"Topic",
				),
				structfield.NewID(
					"github.com/segmentio/kafka-go",
					"github.com/segmentio/kafka-go",
					"Reader",
					"config",
				),
				structfield.NewID(
					"github.com/segmentio/kafka-go",
					"github.com/segmentio/kafka-go",
					"ReaderConfig",
					"GroupID",
				),
				structfield.NewID(
					"github.com/segmentio/kafka-go",
					"github.com/segmentio/kafka-go",
					"Conn",
					"clientID",
				),
			},
		},
	}, nil
}

func main() {
	if err := run(); err != nil {
		os.Exit(1)
	}
}

func run() error {
	m, err := manifests()
	if err != nil {
		logger.Error("failed to load manifests", "error", err)
		return err
	}

	var cache *inspect.Cache
	if cacheFile != "" {
		cache, err = inspect.NewCache(logger, cacheFile)
		if err != nil {
			logger.Error("failed to load cache", "error", err, "path", cacheFile)
			// Use an empty cache.
		}
	}

	i, err := inspect.New(logger, cache, m...)
	if err != nil {
		logger.Error("failed to setup inspector", "error", err)
		return err
	}
	i.NWorkers = numCPU

	// Trap Ctrl+C and call cancel on the context.
	ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
	defer cancel()

	to, err := i.Do(ctx)
	if err != nil {
		logger.Error("failed get offsets", "error", err)
		return err
	}

	if to == nil {
		logger.Info("no offsets found")
		return nil
	}

	logger.Info("writing offsets", "dest", outputFile)
	f, err := os.Create(outputFile)
	if err != nil {
		logger.Error("failed to open output file", "error", err, "dest", outputFile)
		return err
	}
	defer f.Close()

	enc := json.NewEncoder(f)
	enc.SetIndent("", "  ")
	if err := enc.Encode(to); err != nil {
		logger.Error("failed to write offsets", "error", err, "dest", outputFile)
		return err
	}
	return nil
}
