// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

package runtime

import (
	"context"
	"fmt"

	"github.com/cosi-project/runtime/pkg/controller"
	"github.com/cosi-project/runtime/pkg/resource"
	"github.com/cosi-project/runtime/pkg/state"
	"github.com/rs/xid"
	"github.com/siderolabs/go-pointer"
	"github.com/siderolabs/go-procfs/procfs"
	"github.com/siderolabs/siderolink/api/events"
	"go.uber.org/atomic"
	"go.uber.org/zap"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	"google.golang.org/protobuf/types/known/anypb"

	"github.com/siderolabs/talos/internal/app/machined/pkg/runtime"
	"github.com/siderolabs/talos/pkg/machinery/constants"
	"github.com/siderolabs/talos/pkg/machinery/resources/network"
)

// EventsSinkController watches events and forwards them to the events sink server
// if it's configured.
type EventsSinkController struct {
	V1Alpha1Events runtime.Watcher
	Cmdline        *procfs.Cmdline
	Drainer        *runtime.Drainer

	drainSub *runtime.DrainSubscription
	drain    bool
	backlog  atomic.Int32
	eventID  xid.ID
}

// Name implements controller.Controller interface.
func (ctrl *EventsSinkController) Name() string {
	return "v1alpha1.EventsSinkController"
}

// Inputs implements controller.Controller interface.
func (ctrl *EventsSinkController) Inputs() []controller.Input {
	return []controller.Input{
		{
			Namespace: network.NamespaceName,
			Type:      network.StatusType,
			ID:        pointer.To(network.StatusID),
			Kind:      controller.InputWeak,
		},
	}
}

// Outputs implements controller.Controller interface.
func (ctrl *EventsSinkController) Outputs() []controller.Output {
	return nil
}

// Run implements controller.Controller interface.
//
//nolint:gocyclo
func (ctrl *EventsSinkController) Run(ctx context.Context, r controller.Runtime, logger *zap.Logger) (err error) {
	if ctrl.Cmdline == nil || ctrl.Cmdline.Get(constants.KernelParamEventsSink).First() == nil {
		return nil
	}

	if ctrl.drainSub == nil {
		ctrl.backlog.Store(-1)
		ctrl.drainSub = ctrl.Drainer.Subscribe()
	}

	defer func() {
		if ctrl.backlog.Load() == 0 {
			ctrl.drainSub.Cancel()
		}
	}()

	for {
		select {
		case <-ctx.Done():
			return nil
		case <-r.EventCh():
		}

		var netStatus resource.Resource

		netStatus, err = r.Get(ctx, resource.NewMetadata(network.NamespaceName, network.StatusType, network.StatusID, resource.VersionUndefined))
		if err != nil {
			if state.IsNotFoundError(err) {
				// no network state yet
				continue
			}

			return fmt.Errorf("error reading network status: %w", err)
		}

		if !netStatus.(*network.Status).TypedSpec().AddressReady {
			// wait for address
			continue
		}

		break
	}

	errCh := make(chan error)

	sink := ctrl.Cmdline.Get(constants.KernelParamEventsSink).First()

	conn, err := grpc.DialContext(ctx, *sink, grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		return err
	}

	defer conn.Close() //nolint:errcheck

	client := events.NewEventSinkServiceClient(conn)

	opts := []runtime.WatchOptionFunc{}
	if ctrl.eventID.IsNil() {
		opts = append(opts, runtime.WithTailEvents(-1))
	} else {
		opts = append(opts, runtime.WithTailID(ctrl.eventID))
	}

	if err = ctrl.V1Alpha1Events.Watch(func(eventCh <-chan runtime.EventInfo) {
		errCh <- ctrl.handleEvents(ctx, eventCh, client)
	}, opts...); err != nil {
		return err
	}

	err = <-errCh

	return err
}

//nolint:gocyclo
func (ctrl *EventsSinkController) handleEvents(ctx context.Context, eventCh <-chan runtime.EventInfo, client events.EventSinkServiceClient) error {
	for {
		var (
			event runtime.EventInfo
			ok    bool
			data  *anypb.Any
			err   error
		)

		select {
		case <-ctx.Done():
			return nil
		case event, ok = <-eventCh:
			if !ok {
				return nil
			}
		case <-ctrl.drainSub.EventCh():
			backlog := ctrl.backlog.Load()

			if backlog == 0 {
				return nil
			}

			ctrl.drain = true

			continue
		}

		ctrl.backlog.Store(int32(event.Backlog))

		data, err = anypb.New(event.Payload)
		if err != nil {
			return err
		}

		req := &events.EventRequest{
			Id:   event.ID.String(),
			Data: data,
		}

		_, err = client.Publish(ctx, req)
		if err != nil {
			return err
		}

		ctrl.eventID = event.ID

		if ctrl.drain && event.Backlog == 0 {
			return nil
		}
	}
}
