// Package bootstrap wires the IOP Node application using go.uber.org/fx.
package bootstrap

import (
	"context"
	"crypto/ed25519"
	"fmt"
	"io"
	"os"
	stdruntime "runtime"
	"sync"
	"time"

	"go.uber.org/fx"
	"go.uber.org/zap"

	"iop/apps/node/internal/adapters"
	"iop/apps/node/internal/node"
	"iop/apps/node/internal/router"
	"iop/apps/node/internal/store"
	"iop/apps/node/internal/transport"
	"iop/apps/node/internal/workspace"
	"iop/packages/go/config"
	"iop/packages/go/credentiallease"
	"iop/packages/go/events"
	runtime "iop/packages/go/execution"
	"iop/packages/go/observability"
	iop "iop/proto/gen/iop"
)

// readySignalTimeout bounds the post-handler NodeReadyRequest/ack round trip. A
// ready failure is retryable (not a *transport.ConnectError), so the supervisor
// reconnects rather than shutting the node down.
const readySignalTimeout = 10 * time.Second

// DialFunc is the signature used to connect to Edge. Can be replaced in tests.
type DialFunc func(ctx context.Context, addr, token string, logger *zap.Logger) (*transport.RegisterResult, error)

// Option configures Module behaviour; intended for testing only.
type Option func(*moduleOpts)

type moduleOpts struct {
	dialer         DialFunc
	sleeper        func(ctx context.Context, d time.Duration)
	metricsStarter func(port int) error
}

type connectRuntimeOptions struct {
	dialer      DialFunc
	hostOS      func() string
	setHandler  func(*transport.Session, transport.Handler)
	signalReady func(*transport.Session, time.Duration) error
}

func (o connectRuntimeOptions) normalized() connectRuntimeOptions {
	if o.hostOS == nil {
		o.hostOS = func() string { return stdruntime.GOOS }
	}
	if o.setHandler == nil {
		o.setHandler = func(session *transport.Session, handler transport.Handler) { session.SetHandler(handler) }
	}
	if o.signalReady == nil {
		o.signalReady = func(session *transport.Session, timeout time.Duration) error { return session.SignalReady(timeout) }
	}
	return o
}

// WithDialer replaces the transport dial function.
func WithDialer(fn DialFunc) Option {
	return func(o *moduleOpts) { o.dialer = fn }
}

// WithSleeper replaces the inter-retry sleep function.
func WithSleeper(fn func(ctx context.Context, d time.Duration)) Option {
	return func(o *moduleOpts) { o.sleeper = fn }
}

// WithMetricsStarter replaces the metrics listener starter for tests.
func WithMetricsStarter(fn func(port int) error) Option {
	return func(o *moduleOpts) { o.metricsStarter = fn }
}

// runtimeOwner holds one connection's resources and closes them idempotently.
type runtimeOwner struct {
	reg            *runtime.Registry
	sess           *transport.Session
	st             *store.Store
	workspace      *workspace.Runtime
	closeRegistry  func()
	closeWorkspace func()
	closeSession   func()
	closeStore     func()
	once           sync.Once
}

func (r *runtimeOwner) close() {
	r.once.Do(func() {
		if r.closeRegistry != nil {
			r.closeRegistry()
		} else if r.reg != nil {
			_ = r.reg.Stop(context.Background())
		}
		if r.closeWorkspace != nil {
			r.closeWorkspace()
		} else if r.workspace != nil {
			_ = r.workspace.Close()
		}
		if r.closeSession != nil {
			r.closeSession()
		} else if r.sess != nil {
			_ = r.sess.Close()
		}
		if r.closeStore != nil {
			r.closeStore()
		} else if r.st != nil {
			_ = r.st.Close()
		}
	})
}

// connectRuntime dials edge and wires up adapters, store, router, and node handler.
// On any failure after partial allocation the allocated resources are closed.
func connectRuntime(ctx context.Context, cfg *config.NodeConfig, logger *zap.Logger, dialer DialFunc, overrides ...connectRuntimeOptions) (*runtimeOwner, error) {
	opts := connectRuntimeOptions{dialer: dialer}
	if len(overrides) > 0 {
		opts = overrides[0]
		if opts.dialer == nil {
			opts.dialer = dialer
		}
	}
	opts = opts.normalized()
	var (
		result *transport.RegisterResult
		err    error
	)
	if opts.dialer == nil {
		result, err = transport.DialEdgeConfig(ctx, cfg, logger)
	} else {
		result, err = opts.dialer(ctx, cfg.Transport.EdgeAddr, cfg.Transport.Token, logger)
	}
	if err != nil {
		return nil, fmt.Errorf("dial edge: %w", err)
	}
	owner := &runtimeOwner{sess: result.Session}

	set, err := adapters.BuildConfigSet(result.Config, logger)
	if err != nil {
		owner.close()
		return nil, fmt.Errorf("build adapters: %w", err)
	}
	owner.reg = set.Registry

	workspaceRuntime, err := workspace.NewRuntime(result.Config.GetWorkspaces(), opts.hostOS(), logger)
	if err != nil {
		owner.close()
		return nil, fmt.Errorf("workspace catalog: %w", err)
	}
	owner.workspace = workspaceRuntime

	dsn, err := storeDSN()
	if err != nil {
		owner.close()
		return nil, err
	}
	st, err := store.New(dsn, logger)
	if err != nil {
		owner.close()
		return nil, fmt.Errorf("store: %w", err)
	}
	owner.st = st

	if err := set.Registry.Start(ctx); err != nil {
		owner.close()
		return nil, fmt.Errorf("start adapters: %w", err)
	}

	rtr := router.New(set.Registry, logger)
	globalConcurrency := int(result.Config.GetRuntime().GetConcurrency())
	n := node.New(result.NodeID, rtr, st, globalConcurrency, os.Stdout, logger, set)
	n.SetWorkspaceRuntime(workspaceRuntime)
	if cfg.CredentialPlane.Enabled {
		recipientPrivate, loadErr := credentiallease.LoadPrivateKeyFile(cfg.CredentialPlane.RecipientPrivateKey, 32)
		if loadErr != nil {
			owner.close()
			return nil, fmt.Errorf("load credential recipient key: %w", loadErr)
		}
		defer zeroNodeKey(recipientPrivate)
		issuerPublic, loadErr := credentiallease.LoadPublicKeyFile(cfg.CredentialPlane.IssuerPublicKey, ed25519.PublicKeySize)
		if loadErr != nil {
			owner.close()
			return nil, fmt.Errorf("load credential issuer key: %w", loadErr)
		}
		defer zeroNodeKey(issuerPublic)
		consumer, consumerErr := credentiallease.NewConsumer(result.NodeID, cfg.CredentialPlane.RecipientKeyID, recipientPrivate, cfg.CredentialPlane.IssuerKeyID, ed25519.PublicKey(issuerPublic), cfg.CredentialPlane.ReplayCacheSize, nil)
		if consumerErr != nil {
			owner.close()
			return nil, fmt.Errorf("compose credential consumer: %w", consumerErr)
		}
		n.SetCredentialConsumer(consumer)
	}
	if result.Session != nil {
		result.Session.SetEventHandler(func(event *iop.EdgeNodeEvent) {
			printEdgeEvent(os.Stdout, event)
		})
	}
	opts.setHandler(result.Session, n)

	// The handler is installed; only now tell edge we are ready to receive
	// dispatch. Edge opens run/tunnel/command eligibility and pumps any waiter
	// stranded while this node was offline strictly in response to this signal, so
	// the first request can never race ahead of the handler. A failed ready
	// handshake is retryable: close the partial owner and let the supervisor
	// reconnect rather than sit connected-but-unreachable.
	if err := opts.signalReady(result.Session, readySignalTimeout); err != nil {
		owner.close()
		return nil, fmt.Errorf("signal ready: %w", err)
	}

	logger.Info("connected to edge",
		zap.String("node_id", result.NodeID),
		zap.String("alias", result.Alias),
	)
	return owner, nil
}

func zeroNodeKey(value []byte) {
	for i := range value {
		value[i] = 0
	}
}

// Module returns the fx options that wire the node application.
func Module(cfg *config.NodeConfig, opts ...Option) fx.Option {
	mo := &moduleOpts{
		dialer:         nil,
		metricsStarter: observability.ServeMetrics,
		sleeper: func(ctx context.Context, d time.Duration) {
			if d <= 0 {
				return
			}
			timer := time.NewTimer(d)
			select {
			case <-ctx.Done():
				timer.Stop()
			case <-timer.C:
			}
		},
	}
	for _, opt := range opts {
		opt(mo)
	}

	return fx.Options(
		fx.Provide(
			func() *config.NodeConfig { return cfg },

			func(cfg *config.NodeConfig) (*zap.Logger, error) {
				return observability.NewLoggerWithFile(cfg.Logging.Level, cfg.Logging.Pretty, cfg.Logging.Path)
			},
		),

		fx.Invoke(func(lc fx.Lifecycle, cfg *config.NodeConfig, logger *zap.Logger, shutdowner fx.Shutdowner) {
			sup := &runtimeSupervisor{
				cfg:            cfg,
				logger:         logger,
				dialer:         mo.dialer,
				sleeper:        mo.sleeper,
				metricsStarter: mo.metricsStarter,
				shutdowner:     shutdowner,
			}
			lc.Append(fx.Hook{
				OnStart: func(ctx context.Context) error {
					return sup.start(ctx)
				},
				OnStop: func(_ context.Context) error {
					sup.stop()
					return nil
				},
			})
		}),
	)
}

func storeDSN() (string, error) {
	return "file:iop.db?cache=shared&mode=rwc", nil
}

func printEdgeEvent(out io.Writer, event *iop.EdgeNodeEvent) {
	if out == nil {
		return
	}
	switch event.GetType() {
	case events.TypeEdgeDisconnected:
		fmt.Fprintf(out, "[edge-event] disconnected reason=%q%s\n", event.GetReason(), transportCloseDetail(event.GetMetadata()))
	default:
		fmt.Fprintf(out, "[edge-event] %s reason=%q%s\n", event.GetType(), event.GetReason(), transportCloseDetail(event.GetMetadata()))
	}
}

func transportCloseDetail(metadata map[string]string) string {
	if metadata == nil {
		return ""
	}
	detail := ""
	if reason := metadata[events.MetadataTransportCloseReason]; reason != "" {
		detail += fmt.Sprintf(" transport_close_reason=%q", reason)
	}
	if err := metadata[events.MetadataTransportCloseError]; err != "" {
		detail += fmt.Sprintf(" transport_close_error=%q", err)
	}
	return detail
}
