// Copyright 2020-2026 Buf Technologies, Inc.
//
// 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 lspserve defines the entry-point for the Buf LSP within the CLI.
//
// The actual implementation of the LSP lives under private/buf/buflsp
package lspserve

import (
	"context"
	"errors"
	"fmt"
	"io"
	"log/slog"
	"net"

	"buf.build/go/app/appcmd"
	"buf.build/go/app/appext"
	"buf.build/go/standard/xio"
	"connectrpc.com/connect"
	"github.com/bufbuild/buf/private/buf/bufcli"
	"github.com/bufbuild/buf/private/buf/buflsp"
	"github.com/bufbuild/buf/private/gen/proto/connect/buf/alpha/registry/v1alpha1/registryv1alpha1connect"
	registryv1alpha1 "github.com/bufbuild/buf/private/gen/proto/go/buf/alpha/registry/v1alpha1"
	"github.com/bufbuild/buf/private/pkg/connectclient"
	"github.com/bufbuild/protocompile/experimental/incremental"
	"github.com/spf13/pflag"
	"go.lsp.dev/jsonrpc2"
)

const (
	// pipe is chosen because that's what the vscode LSP client expects.
	pipeFlagName         = "pipe"
	debugAddressFlagName = "debug-address"
)

// NewCommand constructs the CLI command for executing the LSP.
func NewCommand(
	name string,
	builder appext.Builder,
	deprecated string,
	hidden bool,
	beta bool,
) *appcmd.Command {
	flags := newFlags()
	return &appcmd.Command{
		Use:        name,
		Short:      "Start the language server",
		Args:       appcmd.NoArgs,
		Deprecated: deprecated,
		Hidden:     hidden,
		Run: builder.NewRunFunc(
			func(ctx context.Context, container appext.Container) error {
				if beta {
					bufcli.WarnBetaCommand(ctx, container)
				}
				return run(ctx, container, flags)
			},
		),
		BindFlags: flags.Bind,
	}
}

type flags struct {
	// A file path to a UNIX socket to use for IPC. If empty, stdio is used instead.
	PipePath string
	// An address (host:port) to serve the debug server on. If empty, no debug server is started.
	DebugAddress string
}

// Bind sets up the CLI flags that the LSP needs.
func (f *flags) Bind(flagSet *pflag.FlagSet) {
	flagSet.StringVar(
		&f.PipePath,
		pipeFlagName,
		"",
		"path to a UNIX socket to listen on; uses stdio if not specified",
	)
	flagSet.StringVar(
		&f.DebugAddress,
		debugAddressFlagName,
		"",
		"address to serve debug endpoints on (e.g. localhost:6060); disabled if not specified",
	)
}

func newFlags() *flags {
	return &flags{}
}

// run starts the LSP server and listens on the configured.
func run(
	ctx context.Context,
	container appext.Container,
	flags *flags,
) (retErr error) {
	if flags.DebugAddress != "" {
		server, err := newDebugServer(flags.DebugAddress, bufcli.Version)
		if err != nil {
			return err
		}
		container.Logger().Info(
			"debug server listening",
			slog.String("address", server.Addr().String()),
		)
		defer func() {
			retErr = errors.Join(retErr, server.Close())
		}()
	}

	transport, err := dial(container, flags)
	if err != nil {
		return err
	}

	wktStore, err := bufcli.NewWKTStore(container)
	if err != nil {
		return err
	}
	wktBucket, err := wktStore.GetBucket(ctx)
	if err != nil {
		return err
	}

	controller, err := bufcli.NewController(container)
	if err != nil {
		return err
	}

	wasmRuntime, err := bufcli.NewWasmRuntime(ctx, container)
	if err != nil {
		return err
	}
	defer func() {
		retErr = errors.Join(retErr, wasmRuntime.Close(ctx))
	}()

	moduleKeyProvider, err := bufcli.NewModuleKeyProvider(container)
	if err != nil {
		return err
	}

	graphProvider, err := bufcli.NewGraphProvider(container)
	if err != nil {
		return err
	}

	clientConfig, err := bufcli.NewConnectClientConfig(container)
	if err != nil {
		return err
	}

	conn, err := buflsp.Serve(
		ctx,
		bufcli.Version,
		wktBucket,
		container,
		controller,
		wasmRuntime,
		jsonrpc2.NewStream(transport),
		incremental.New(),
		moduleKeyProvider,
		graphProvider,
		&lspCuratedPluginProvider{clientConfig: clientConfig},
	)
	if err != nil {
		return err
	}
	<-conn.Done()
	return conn.Err()
}

type lspCuratedPluginProvider struct {
	clientConfig *connectclient.Config
}

func (p *lspCuratedPluginProvider) GetLatestVersion(ctx context.Context, registry, owner, plugin string) (string, error) {
	client := connectclient.Make(p.clientConfig, registry, registryv1alpha1connect.NewPluginCurationServiceClient)
	resp, err := client.GetLatestCuratedPlugin(ctx, connect.NewRequest(
		registryv1alpha1.GetLatestCuratedPluginRequest_builder{
			Owner: owner,
			Name:  plugin,
		}.Build(),
	))
	if err != nil {
		return "", err
	}
	if !resp.Msg.HasPlugin() {
		return "", nil
	}
	return resp.Msg.GetPlugin().GetVersion(), nil
}

// dial opens a connection to the LSP client.
func dial(container appext.Container, flags *flags) (io.ReadWriteCloser, error) {
	switch {
	case flags.PipePath != "":
		conn, err := net.Dial("unix", flags.PipePath)
		if err != nil {
			return nil, fmt.Errorf("could not open IPC socket %q: %w", flags.PipePath, err)
		}
		return conn, nil

	// TODO: Add other transport implementations, such as TCP, here!

	default:
		// Fall back to stdio by default.
		return xio.CompositeReadWriteCloser(
			container.Stdin(),
			container.Stdout(),
			xio.NopCloser,
		), nil
	}
}
