// Package daemon implements the isle daemon, which is a long-running service
// managing all isle background tasks and sub-processes for a single network.
package daemon

import (
	"context"
	"errors"
	"fmt"
	"isle/bootstrap"
	"isle/daemon/daecommon"
	"isle/daemon/network"
	"isle/nebula"
	"sort"
	"sync"

	"dev.mediocregopher.com/mediocre-go-lib.git/mlog"
)

var _ RPC = (*Daemon)(nil)

type joinedNetwork struct {
	id string
	network.Network
	creationParams bootstrap.CreationParams
	config         *daecommon.NetworkConfig
}

// Daemon implements all methods of the Daemon interface, plus others used
// to manage this particular implementation.
//
// Daemon manages all child processes and state required by the isle
// service, as well as an HTTP socket over which RPC calls will be served.
//
// Inner Children instance(s) will be wrapped such that they will be
// automatically shutdown and re-created whenever there's changes in the network
// which require the configuration to be changed (e.g. a new nebula lighthouse).
// During such an inner restart all methods will return ErrRestarting, except
// Shutdown which will block until the currently executing restart is finished
// and then shutdown cleanly from there.
//
// While still starting up the Daemon for the first time all methods will return
// ErrInitializing, except Shutdown which will block until initialization is
// canceled.
type Daemon struct {
	logger        *mlog.Logger
	networkLoader network.Loader
	daemonConfig  daecommon.Config

	l        sync.RWMutex
	networks map[string]joinedNetwork
}

// New initializes and returns a Daemon.
func New(
	ctx context.Context,
	logger *mlog.Logger,
	networkLoader network.Loader,
	daemonConfig daecommon.Config,
) (
	*Daemon, error,
) {
	d := &Daemon{
		logger:        logger,
		networkLoader: networkLoader,
		daemonConfig:  daemonConfig,
		networks:      map[string]joinedNetwork{},
	}

	loadableNetworks, err := networkLoader.Loadable(ctx)
	if err != nil {
		return nil, fmt.Errorf("listing loadable networks: %w", err)
	}

	if getDeprecatedNetworkConfig(daemonConfig.Networks) != nil &&
		len(loadableNetworks) > 1 {
		return nil, errors.New("Deprecated config format cannot be used when there are more than one loadable networks")
	}

	if err := validateConfig(
		ctx, networkLoader, daemonConfig, loadableNetworks,
	); err != nil {
		return nil, fmt.Errorf("validating daemon config: %w", err)
	}

	for _, creationParams := range loadableNetworks {
		var (
			id            = creationParams.ID
			networkConfig = pickNetworkConfig(
				daemonConfig.Networks, creationParams,
			)
		)

		n, err := networkLoader.Load(
			ctx,
			networkLogger(logger, creationParams),
			creationParams,
			&network.Opts{
				Config: networkConfig,
			},
		)
		if err != nil {
			return nil, fmt.Errorf("loading network %q: %w", id, err)
		}

		d.networks[id] = joinedNetwork{id, n, creationParams, networkConfig}
	}

	return d, nil
}

// CreateNetwork will initialize a new network using the given parameters.
//   - name: Human-readable name of the network.
//   - domain: Primary domain name that network services are served under.
//   - ipNet: An IP subnet, in CIDR form, which will be the overall range of
//     possible IPs in the network. The first IP in this network range will become
//     this first host's IP.
//   - hostName: The name of this first host in the network.
//
// The daemon on which this is called will become the first host in the network,
// and will have full administrative privileges.
//
// Errors:
//   - [network.ErrInvalidConfig]
//   - [ErrAlreadyJoined]
//   - [ErrManagedNetworkConfig] - if `opts.NetworkConfig` is given, but a
//     NetworkConfig is also provided in the Daemon's Config.
func (d *Daemon) CreateNetwork(
	ctx context.Context,
	creationParams bootstrap.CreationParams,
	ipNet nebula.IPNet,
	hostName nebula.HostName,
	opts *CreateNetworkOpts,
) error {
	opts = opts.withDefaults()

	d.l.Lock()
	defer d.l.Unlock()

	if getDeprecatedNetworkConfig(d.daemonConfig.Networks) != nil &&
		len(d.networks) > 0 {
		return network.ErrInvalidConfig.WithData(
			"Cannot use deprecated configuration file format while joined to more than one network",
		)
	}

	var (
		networkConfig = pickNetworkConfig(
			d.daemonConfig.Networks, creationParams,
		)
		networkLogger = networkLogger(d.logger, creationParams)
	)

	if opts.Config != nil {
		if networkConfig != nil {
			return ErrManagedNetworkConfig
		}
		networkConfig = opts.Config
	}

	if alreadyJoined(d.networks, creationParams) {
		return ErrAlreadyJoined
	}

	networkLogger.Info(ctx, "Creating network")
	n, err := d.networkLoader.Create(
		ctx,
		networkLogger,
		creationParams,
		ipNet,
		hostName,
		&network.Opts{
			Config: networkConfig,
		},
	)
	if err != nil {
		return fmt.Errorf("creating network: %w", err)
	}

	networkLogger.Info(ctx, "Network created successfully")
	d.networks[creationParams.ID] = joinedNetwork{
		creationParams.ID, n, creationParams, networkConfig,
	}
	return nil
}

// JoinNetwork joins the Daemon to an existing network using the given
// Bootstrap.
//
// Errors:
// - ErrAlreadyJoined
func (d *Daemon) JoinNetwork(
	ctx context.Context, newBootstrap network.JoiningBootstrap,
) error {
	d.l.Lock()
	defer d.l.Unlock()

	if getDeprecatedNetworkConfig(d.daemonConfig.Networks) != nil &&
		len(d.networks) > 0 {
		return network.ErrInvalidConfig.WithData(
			"Cannot use deprecated configuration file format while joined to more than one network",
		)
	}

	var (
		creationParams = newBootstrap.Bootstrap.NetworkCreationParams
		networkID      = creationParams.ID
		networkConfig  = pickNetworkConfig(
			d.daemonConfig.Networks, creationParams,
		)
		networkLogger = networkLogger(
			d.logger, newBootstrap.Bootstrap.NetworkCreationParams,
		)
	)

	if alreadyJoined(d.networks, creationParams) {
		return ErrAlreadyJoined
	}

	networkLogger.Info(ctx, "Joining network")
	n, err := d.networkLoader.Join(
		ctx,
		networkLogger,
		newBootstrap,
		&network.Opts{
			Config: networkConfig,
		},
	)
	if err != nil {
		return fmt.Errorf(
			"joining network %q: %w", networkID, err,
		)
	}

	networkLogger.Info(ctx, "Network joined successfully")
	d.networks[networkID] = joinedNetwork{
		networkID, n, creationParams, networkConfig,
	}
	return nil
}

func withNetwork[Res any](
	ctx context.Context,
	d *Daemon,
	fn func(context.Context, joinedNetwork) (Res, error),
) (
	Res, error,
) {
	d.l.RLock()
	defer d.l.RUnlock()

	network, err := pickNetwork(ctx, d.networkLoader, d.networks)
	if err != nil {
		var zero Res
		return zero, err
	}

	return fn(ctx, network)
}

// LeaveNetwork picks the network out of the Context which was embedded by
// WithNetwork, and leaves it. The Network will no longer be considered joined
// and will not be active after this returns.
//
// Errors:
// - ErrNoNetwork
// - ErrNoMatchingNetworks
// - ErrMultipleMatchingNetworks
func (d *Daemon) LeaveNetwork(ctx context.Context) error {
	d.l.Lock()
	defer d.l.Unlock()

	network, err := pickNetwork(ctx, d.networkLoader, d.networks)
	if err != nil {
		return err
	}

	shutdownErr := network.Shutdown()
	loaderLeaveErr := d.networkLoader.Leave(ctx, network.creationParams)
	delete(d.networks, network.creationParams.ID)

	return errors.Join(shutdownErr, loaderLeaveErr)
}

// GetNetworks returns all networks which have been joined by the Daemon,
// ordered by their name.
func (d *Daemon) GetNetworks(
	ctx context.Context,
) (
	[]bootstrap.CreationParams, error,
) {
	d.l.RLock()
	defer d.l.RUnlock()

	res := make([]bootstrap.CreationParams, 0, len(d.networks))
	for id, network := range d.networks {
		creationParams, err := network.GetNetworkCreationParams(ctx)
		if err != nil {
			return nil, fmt.Errorf(
				"getting network creation params of network %q: %w", id, err,
			)
		}

		res = append(res, creationParams)
	}

	sort.Slice(res, func(i, j int) bool {
		return res[i].Name < res[j].Name
	})

	return res, nil
}

// GetBootstrap implements the method for the network.RPC interface.
func (d *Daemon) GetBootstrap(
	ctx context.Context,
) (
	bootstrap.Bootstrap, error,
) {
	return withNetwork(
		ctx,
		d,
		func(
			ctx context.Context, n joinedNetwork,
		) (
			bootstrap.Bootstrap, error,
		) {
			return n.GetBootstrap(ctx)
		},
	)
}

// GetGarageClientParams implements the method for the network.RPC interface.
func (d *Daemon) GetGarageClientParams(
	ctx context.Context,
) (
	network.GarageClientParams, error,
) {
	return withNetwork(
		ctx,
		d,
		func(
			ctx context.Context, n joinedNetwork,
		) (
			network.GarageClientParams, error,
		) {
			return n.GetGarageClientParams(ctx)
		},
	)
}

// RemoveHost implements the method for the network.RPC interface.
func (d *Daemon) RemoveHost(ctx context.Context, hostName nebula.HostName) error {
	_, err := withNetwork(
		ctx,
		d,
		func(
			ctx context.Context, n joinedNetwork,
		) (
			struct{}, error,
		) {
			return struct{}{}, n.RemoveHost(ctx, hostName)
		},
	)
	return err
}

// CreateHost implements the method for the network.RPC interface.
func (d *Daemon) CreateHost(
	ctx context.Context,
	hostName nebula.HostName,
	opts network.CreateHostOpts,
) (
	network.JoiningBootstrap, error,
) {
	return withNetwork(
		ctx,
		d,
		func(
			ctx context.Context, n joinedNetwork,
		) (
			network.JoiningBootstrap, error,
		) {
			return n.CreateHost(ctx, hostName, opts)
		},
	)
}

// CreateNebulaCertificate implements the method for the network.RPC interface.
func (d *Daemon) CreateNebulaCertificate(
	ctx context.Context,
	hostName nebula.HostName,
	hostPubKey nebula.EncryptingPublicKey,
) (
	nebula.Certificate, error,
) {
	return withNetwork(
		ctx,
		d,
		func(
			ctx context.Context, n joinedNetwork,
		) (
			nebula.Certificate, error,
		) {
			return n.CreateNebulaCertificate(ctx, hostName, hostPubKey)
		},
	)
}

// GetConfig implements the method for the network.RPC interface.
func (d *Daemon) GetConfig(
	ctx context.Context,
) (
	daecommon.NetworkConfig, error,
) {
	return withNetwork(
		ctx,
		d,
		func(
			ctx context.Context, n joinedNetwork,
		) (
			daecommon.NetworkConfig, error,
		) {
			return n.GetConfig(ctx)
		},
	)
}

// SetConfig extends the [network.RPC] method of the same name such that
// [ErrManagedNetworkConfig] is returned if the picked network is
// configured as part of the [daecommon.Config] which the Daemon was
// initialized with.
//
// See the `network.RPC` documentation in this interface for more usage
// details.
func (d *Daemon) SetConfig(
	ctx context.Context, networkConfig daecommon.NetworkConfig,
) error {
	d.l.RLock()
	defer d.l.RUnlock()

	pickedNetwork, err := pickNetwork(ctx, d.networkLoader, d.networks)
	if err != nil {
		return err
	}

	if pickedNetwork.config != nil {
		return ErrManagedNetworkConfig
	}

	// Reconstruct the daemon config using the actual in-use network configs,
	// along with this new one, and do a validation before calling SetConfig.

	daemonConfig := d.daemonConfig
	daemonConfig.Networks = map[string]daecommon.NetworkConfig{
		pickedNetwork.id: networkConfig,
	}

	for id, joinedNetwork := range d.networks {
		if pickedNetwork.id == id {
			continue
		}

		joinedNetworkConfig, err := joinedNetwork.GetConfig(ctx)
		if err != nil {
			return fmt.Errorf("getting network config of %q: %w", id, err)
		}

		daemonConfig.Networks[id] = joinedNetworkConfig
	}

	if err := daemonConfig.Validate(); err != nil {
		return network.ErrInvalidConfig.WithData(err.Error())
	}

	return pickedNetwork.SetConfig(ctx, networkConfig)
}

// Shutdown blocks until all resources held or created by the daemon,
// including child processes it has started, have been cleaned up.
//
// If this returns an error then it's possible that child processes are
// still running and are no longer managed.
func (d *Daemon) Shutdown() error {
	d.l.Lock()
	defer d.l.Unlock()

	var (
		errCh = make(chan error, len(d.networks))
		errs  []error
	)

	for id := range d.networks {
		var (
			id = id
			n  = d.networks[id]
		)

		go func() {
			if err := n.Shutdown(); err != nil {
				errCh <- fmt.Errorf("shutting down network %q: %w", id, err)
			}
			errCh <- nil
		}()
	}

	for range cap(errCh) {
		errs = append(errs, <-errCh)
	}

	return errors.Join(errs...)
}