package netann

import (
	"errors"
	"sync"
	"time"

	"github.com/btcsuite/btcd/btcec/v2"
	"github.com/btcsuite/btcd/wire"
	"github.com/lightningnetwork/lnd/channeldb"
	"github.com/lightningnetwork/lnd/keychain"
	"github.com/lightningnetwork/lnd/lnwallet"
	"github.com/lightningnetwork/lnd/lnwire"
)

var (
	// ErrChanStatusManagerExiting signals that a shutdown of the
	// ChanStatusManager has already been requested.
	ErrChanStatusManagerExiting = errors.New("chan status manager exiting")

	// ErrInvalidTimeoutConstraints signals that the ChanStatusManager could
	// not be initialized because the timeouts and sample intervals were
	// malformed.
	ErrInvalidTimeoutConstraints = errors.New("chan-enable-timeout + " +
		"chan-status-sample-interval must <= chan-disable-timeout " +
		"and all three chan configs must be positive integers")

	// ErrEnableInactiveChan signals that a request to enable a channel
	// could not be completed because the channel isn't actually active at
	// the time of the request.
	ErrEnableInactiveChan = errors.New("unable to enable channel which " +
		"is not currently active")

	// ErrEnableManuallyDisabledChan signals that an automatic / background
	// request to enable a channel could not be completed because the channel
	// was manually disabled.
	ErrEnableManuallyDisabledChan = errors.New("unable to enable channel " +
		"which was manually disabled")
)

// ChanStatusConfig holds parameters and resources required by the
// ChanStatusManager to perform its duty.
type ChanStatusConfig struct {
	// OurPubKey is the public key identifying this node on the network.
	OurPubKey *btcec.PublicKey

	// OurKeyLoc is the locator for the public key identifying this node on
	// the network.
	OurKeyLoc keychain.KeyLocator

	// MessageSigner signs messages that validate under OurPubKey.
	MessageSigner lnwallet.MessageSigner

	// IsChannelActive checks whether the channel identified by the provided
	// ChannelID is considered active. This should only return true if the
	// channel has been sufficiently confirmed, the channel has received
	// ChannelReady, and the remote peer is online.
	IsChannelActive func(lnwire.ChannelID) bool

	// ApplyChannelUpdate processes new ChannelUpdates signed by our node by
	// updating our local routing table and broadcasting the update to our
	// peers.
	ApplyChannelUpdate func(*lnwire.ChannelUpdate1, *wire.OutPoint,
		bool) error

	// DB stores the set of channels that are to be monitored.
	DB DB

	// Graph stores the channel info and policies for channels in DB.
	Graph ChannelGraph

	// ChanEnableTimeout is the duration a peer's connect must remain stable
	// before attempting to re-enable the channel.
	//
	// NOTE: This value is only used to verify that the relation between
	// itself, ChanDisableTimeout, and ChanStatusSampleInterval is correct.
	// The user is still responsible for ensuring that the same duration
	// elapses before attempting to re-enable a channel.
	ChanEnableTimeout time.Duration

	// ChanDisableTimeout is the duration the manager will wait after
	// detecting that a channel has become inactive before broadcasting an
	// update to disable the channel.
	ChanDisableTimeout time.Duration

	// ChanStatusSampleInterval is the long-polling interval used by the
	// manager to check if the channels being monitored have become
	// inactive.
	ChanStatusSampleInterval time.Duration
}

// ChanStatusManager facilitates requests to enable or disable a channel via a
// network announcement that sets the disable bit on the ChannelUpdate
// accordingly. The manager will periodically sample to detect cases where a
// link has become inactive, and facilitate the process of disabling the channel
// passively. The ChanStatusManager state machine is designed to reduce the
// likelihood of spamming the network with updates for flapping peers.
type ChanStatusManager struct {
	started sync.Once
	stopped sync.Once

	cfg *ChanStatusConfig

	// ourPubKeyBytes is the serialized compressed pubkey of our node.
	ourPubKeyBytes []byte

	// chanStates contains the set of channels being monitored for status
	// updates. Access to the map is serialized by the statusManager's event
	// loop.
	chanStates channelStates

	// enableRequests pipes external requests to enable a channel into the
	// primary event loop.
	enableRequests chan statusRequest

	// disableRequests pipes external requests to disable a channel into the
	// primary event loop.
	disableRequests chan statusRequest

	// autoRequests pipes external requests to restore automatic channel
	// state management into the primary event loop.
	autoRequests chan statusRequest

	// statusSampleTicker fires at the interval prescribed by
	// ChanStatusSampleInterval to check if channels in chanStates have
	// become inactive.
	statusSampleTicker *time.Ticker

	wg   sync.WaitGroup
	quit chan struct{}
}

// NewChanStatusManager initializes a new ChanStatusManager using the given
// configuration. An error is returned if the timeouts and sample interval fail
// to meet do not satisfy the equation:
//
//	ChanEnableTimeout + ChanStatusSampleInterval > ChanDisableTimeout.
func NewChanStatusManager(cfg *ChanStatusConfig) (*ChanStatusManager, error) {
	// Assert that the config timeouts are properly formed. We require the
	// enable_timeout + sample_interval to be less than or equal to the
	// disable_timeout and that all are positive values. A peer that
	// disconnects and reconnects quickly may cause a disable update to be
	// sent, shortly followed by a re-enable. Ensuring a healthy separation
	// helps dampen the possibility of spamming updates that toggle the
	// disable bit for such events.
	if cfg.ChanStatusSampleInterval <= 0 {
		return nil, ErrInvalidTimeoutConstraints
	}
	if cfg.ChanEnableTimeout <= 0 {
		return nil, ErrInvalidTimeoutConstraints
	}
	if cfg.ChanDisableTimeout <= 0 {
		return nil, ErrInvalidTimeoutConstraints
	}
	if cfg.ChanEnableTimeout+cfg.ChanStatusSampleInterval >
		cfg.ChanDisableTimeout {
		return nil, ErrInvalidTimeoutConstraints

	}

	return &ChanStatusManager{
		cfg:                cfg,
		ourPubKeyBytes:     cfg.OurPubKey.SerializeCompressed(),
		chanStates:         make(channelStates),
		statusSampleTicker: time.NewTicker(cfg.ChanStatusSampleInterval),
		enableRequests:     make(chan statusRequest),
		disableRequests:    make(chan statusRequest),
		autoRequests:       make(chan statusRequest),
		quit:               make(chan struct{}),
	}, nil
}

// Start safely starts the ChanStatusManager.
func (m *ChanStatusManager) Start() error {
	var err error
	m.started.Do(func() {
		log.Info("Channel Status Manager starting")
		err = m.start()
	})
	return err
}

func (m *ChanStatusManager) start() error {
	channels, err := m.fetchChannels()
	if err != nil {
		return err
	}

	// Populate the initial states of all confirmed, public channels.
	for _, c := range channels {
		_, err := m.getOrInitChanStatus(c.FundingOutpoint)
		switch {

		// If we can't retrieve the edge info for this channel, it may
		// have been pruned from the channel graph but not yet from our
		// set of channels. We'll skip it as we can't determine its
		// initial state.
		case errors.Is(err, channeldb.ErrEdgeNotFound):
			log.Warnf("Unable to find channel policies for %v, "+
				"skipping. This is typical if the channel is "+
				"in the process of closing.", c.FundingOutpoint)
			continue

		// If we are in the process of opening a channel, the funding
		// manager might not have added the ChannelUpdate to the graph
		// yet. We'll ignore the channel for now.
		case err == ErrUnableToExtractChanUpdate:
			log.Warnf("Unable to find channel policies for %v, "+
				"skipping. This is typical if the channel is "+
				"in the process of being opened.",
				c.FundingOutpoint)
			continue

		case err != nil:
			return err
		}
	}

	m.wg.Add(1)
	go m.statusManager()

	return nil
}

// Stop safely shuts down the ChanStatusManager.
func (m *ChanStatusManager) Stop() error {
	m.stopped.Do(func() {
		log.Info("Channel Status Manager shutting down...")
		defer log.Debug("Channel Status Manager shutdown complete")

		close(m.quit)
		m.wg.Wait()
	})
	return nil
}

// RequestEnable submits a request to immediately enable a channel identified by
// the provided outpoint. If the channel is already enabled, no action will be
// taken. If the channel is marked pending-disable the channel will be returned
// to an active status as the scheduled disable was never sent. Otherwise if the
// channel is found to be disabled, a new announcement will be signed with the
// disabled bit cleared and broadcast to the network.
//
// If the channel was manually disabled and RequestEnable is called with
// manual = false, then the request will be ignored.
//
// NOTE: RequestEnable should only be called after a stable connection with the
// channel's peer has lasted at least the ChanEnableTimeout. Failure to do so
// may result in behavior that deviates from the expected behavior of the state
// machine.
func (m *ChanStatusManager) RequestEnable(outpoint wire.OutPoint,
	manual bool) error {

	return m.submitRequest(m.enableRequests, outpoint, manual)
}

// RequestDisable submits a request to immediately disable a channel identified
// by the provided outpoint. If the channel is already disabled, no action will
// be taken. Otherwise, a new announcement will be signed with the disabled bit
// set and broadcast to the network.
//
// The channel state will be changed to either ChanStatusDisabled or
// ChanStatusManuallyDisabled, depending on the passed-in value of manual. In
// particular, note the following state transitions:
//
//	current state    | manual | new state
//	---------------------------------------------------
//	Disabled         | false  | Disabled
//	ManuallyDisabled | false  | ManuallyDisabled (*)
//	Disabled         | true   | ManuallyDisabled
//	ManuallyDisabled | true   | ManuallyDisabled
//
// (*) If a channel was manually disabled, subsequent automatic / background
//
//	requests to disable the channel do not change the fact that the channel
//	was manually disabled.
func (m *ChanStatusManager) RequestDisable(outpoint wire.OutPoint,
	manual bool) error {

	return m.submitRequest(m.disableRequests, outpoint, manual)
}

// RequestAuto submits a request to restore automatic channel state management.
// If the channel is in the state ChanStatusManuallyDisabled, it will be moved
// back to the state ChanStatusDisabled. Otherwise, no action will be taken.
func (m *ChanStatusManager) RequestAuto(outpoint wire.OutPoint) error {
	return m.submitRequest(m.autoRequests, outpoint, true)
}

// statusRequest is passed to the statusManager to request a change in status
// for a particular channel point.  The exact action is governed by passing the
// request through one of the enableRequests or disableRequests channels.
type statusRequest struct {
	outpoint wire.OutPoint
	manual   bool
	errChan  chan error
}

// submitRequest sends a request for either enabling or disabling a particular
// outpoint and awaits an error response. The request type is dictated by the
// reqChan passed in, which can be either of the enableRequests or
// disableRequests channels.
func (m *ChanStatusManager) submitRequest(reqChan chan statusRequest,
	outpoint wire.OutPoint, manual bool) error {

	req := statusRequest{
		outpoint: outpoint,
		manual:   manual,
		errChan:  make(chan error, 1),
	}

	select {
	case reqChan <- req:
	case <-m.quit:
		return ErrChanStatusManagerExiting
	}

	select {
	case err := <-req.errChan:
		return err
	case <-m.quit:
		return ErrChanStatusManagerExiting
	}
}

// statusManager is the primary event loop for the ChanStatusManager, providing
// the necessary synchronization primitive to protect access to the chanStates
// map. All requests to explicitly enable or disable a channel are processed
// within this method. The statusManager will also periodically poll the active
// status of channels within the htlcswitch to see if a disable announcement
// should be scheduled or broadcast.
//
// NOTE: This method MUST be run as a goroutine.
func (m *ChanStatusManager) statusManager() {
	defer m.wg.Done()

	for {
		select {

		// Process any requests to mark channel as enabled.
		case req := <-m.enableRequests:
			req.errChan <- m.processEnableRequest(req.outpoint, req.manual)

		// Process any requests to mark channel as disabled.
		case req := <-m.disableRequests:
			req.errChan <- m.processDisableRequest(req.outpoint, req.manual)

		// Process any requests to restore automatic channel state management.
		case req := <-m.autoRequests:
			req.errChan <- m.processAutoRequest(req.outpoint)

		// Use long-polling to detect when channels become inactive.
		case <-m.statusSampleTicker.C:
			// First, do a sweep and mark any ChanStatusEnabled
			// channels that are not active within the htlcswitch as
			// ChanStatusPendingDisabled. The channel will then be
			// disabled if no request to enable is received before
			// the ChanDisableTimeout expires.
			m.markPendingInactiveChannels()

			// Now, do another sweep to disable any channels that
			// were marked in a prior iteration as pending inactive
			// if the inactive chan timeout has elapsed.
			m.disableInactiveChannels()

		case <-m.quit:
			return
		}
	}
}

// processEnableRequest attempts to enable the given outpoint.
//
//   - If the channel is not active at the time of the request,
//     ErrEnableInactiveChan will be returned.
//   - If the channel was in the ManuallyDisabled state and manual = false,
//     the request will be ignored and ErrEnableManuallyDisabledChan will be
//     returned.
//   - Otherwise, the status of the channel in chanStates will be
//     ChanStatusEnabled and the method will return nil.
//
// An update will be broadcast only if the channel is currently disabled,
// otherwise no update will be sent on the network.
func (m *ChanStatusManager) processEnableRequest(outpoint wire.OutPoint,
	manual bool) error {

	curState, err := m.getOrInitChanStatus(outpoint)
	if err != nil {
		return err
	}

	// Quickly check to see if the requested channel is active within the
	// htlcswitch and return an error if it isn't.
	chanID := lnwire.NewChanIDFromOutPoint(outpoint)
	if !m.cfg.IsChannelActive(chanID) {
		return ErrEnableInactiveChan
	}

	switch curState.Status {

	// Channel is already enabled, nothing to do.
	case ChanStatusEnabled:
		log.Debugf("Channel(%v) already enabled, skipped "+
			"announcement", outpoint)

		return nil

	// The channel is enabled, though we are now canceling the scheduled
	// disable.
	case ChanStatusPendingDisabled:
		log.Debugf("Channel(%v) already enabled, canceling scheduled "+
			"disable", outpoint)

	// We'll sign a new update if the channel is still disabled.
	case ChanStatusManuallyDisabled:
		if !manual {
			return ErrEnableManuallyDisabledChan
		}
		fallthrough

	case ChanStatusDisabled:
		log.Infof("Announcing channel(%v) enabled", outpoint)

		err := m.signAndSendNextUpdate(outpoint, false)
		if err != nil {
			return err
		}
	}

	m.chanStates.markEnabled(outpoint)

	return nil
}

// processDisableRequest attempts to disable the given outpoint. If the method
// returns nil, the status of the channel in chanStates will be either
// ChanStatusDisabled or ChanStatusManuallyDisabled, depending on the
// passed-in value of manual.
//
// An update will only be sent if the channel has a status other than
// ChanStatusEnabled, otherwise no update will be sent on the network.
func (m *ChanStatusManager) processDisableRequest(outpoint wire.OutPoint,
	manual bool) error {

	curState, err := m.getOrInitChanStatus(outpoint)
	if err != nil {
		return err
	}

	status := curState.Status
	if status == ChanStatusEnabled || status == ChanStatusPendingDisabled {
		log.Infof("Announcing channel(%v) disabled [requested]",
			outpoint)

		err := m.signAndSendNextUpdate(outpoint, true)
		if err != nil {
			return err
		}
	}

	// Typically, a request to disable a channel via the manager's public
	// interface signals that the channel is being closed.
	//
	// If we don't need to keep track of a manual request to disable the
	// channel, then we can remove the outpoint to free up space in the map
	// of channel states. If for some reason the channel isn't closed, the
	// state will be repopulated on subsequent calls to the manager's public
	// interface via a db lookup, or on startup.
	if manual {
		m.chanStates.markManuallyDisabled(outpoint)
	} else if status != ChanStatusManuallyDisabled {
		delete(m.chanStates, outpoint)
	}

	return nil
}

// processAutoRequest attempts to restore automatic channel state management
// for the given outpoint. If the method returns nil, the state of the channel
// will no longer be ChanStatusManuallyDisabled (currently the only state in
// which automatic / background requests are ignored).
//
// No update will be sent on the network.
func (m *ChanStatusManager) processAutoRequest(outpoint wire.OutPoint) error {
	curState, err := m.getOrInitChanStatus(outpoint)
	if err != nil {
		return err
	}

	if curState.Status == ChanStatusManuallyDisabled {
		log.Debugf("Restoring automatic control for manually disabled "+
			"channel(%v)", outpoint)

		m.chanStates.markDisabled(outpoint)
	}
	return nil
}

// markPendingInactiveChannels performs a sweep of the database's active
// channels and determines which, if any, should have a disable announcement
// scheduled. Once an active channel is determined to be pending-inactive, one
// of two transitions can follow. Either the channel is disabled because no
// request to enable is received before the scheduled disable is broadcast, or
// the channel is successfully re-enabled and channel is returned to an active
// state from the POV of the ChanStatusManager.
func (m *ChanStatusManager) markPendingInactiveChannels() {
	channels, err := m.fetchChannels()
	if err != nil {
		log.Errorf("Unable to load active channels: %v", err)
		return
	}

	for _, c := range channels {
		// Determine the initial status of the active channel, and
		// populate the entry in the chanStates map.
		curState, err := m.getOrInitChanStatus(c.FundingOutpoint)
		if err != nil {
			log.Errorf("Unable to retrieve chan status for "+
				"Channel(%v): %v", c.FundingOutpoint, err)
			continue
		}

		// If the channel's status is not ChanStatusEnabled, we are
		// done.  Either it is already disabled, or it has been marked
		// ChanStatusPendingDisable meaning that we have already
		// scheduled the time at which it will be disabled.
		if curState.Status != ChanStatusEnabled {
			continue
		}

		// If our bookkeeping shows the channel as active, sample the
		// htlcswitch to see if it believes the link is also active. If
		// so, we will skip marking it as ChanStatusPendingDisabled.
		chanID := lnwire.NewChanIDFromOutPoint(c.FundingOutpoint)
		if m.cfg.IsChannelActive(chanID) {
			continue
		}

		// Otherwise, we discovered that this link was inactive within
		// the switch. Compute the time at which we will send out a
		// disable if the peer is unable to reestablish a stable
		// connection.
		disableTime := time.Now().Add(m.cfg.ChanDisableTimeout)

		log.Debugf("Marking channel(%v) pending-inactive",
			c.FundingOutpoint)

		m.chanStates.markPendingDisabled(c.FundingOutpoint, disableTime)
	}
}

// disableInactiveChannels scans through the set of monitored channels, and
// broadcast a disable update for any pending inactive channels whose
// SendDisableTime has been superseded by the current time.
func (m *ChanStatusManager) disableInactiveChannels() {
	// Now, disable any channels whose inactive chan timeout has elapsed.
	now := time.Now()
	for outpoint, state := range m.chanStates {
		// Ignore statuses that are not in the pending-inactive state.
		if state.Status != ChanStatusPendingDisabled {
			continue
		}

		// Ignore statuses for which the disable timeout has not
		// expired.
		if state.SendDisableTime.After(now) {
			continue
		}

		log.Infof("Announcing channel(%v) disabled "+
			"[detected]", outpoint)

		// Sign an update disabling the channel.
		err := m.signAndSendNextUpdate(outpoint, true)
		if err != nil {
			log.Errorf("Unable to sign update disabling "+
				"channel(%v): %v", outpoint, err)

			// If the edge was not found, this is a likely indicator
			// that the channel has been closed. Thus we remove the
			// outpoint from the set of tracked outpoints to prevent
			// further attempts.
			if errors.Is(err, channeldb.ErrEdgeNotFound) {
				log.Debugf("Removing channel(%v) from "+
					"consideration for passive disabling",
					outpoint)
				delete(m.chanStates, outpoint)
			}

			continue
		}

		// Record that the channel has now been disabled.
		m.chanStates.markDisabled(outpoint)
	}
}

// fetchChannels returns the working set of channels managed by the
// ChanStatusManager. The returned channels are filtered to only contain public
// channels.
func (m *ChanStatusManager) fetchChannels() ([]*channeldb.OpenChannel, error) {
	allChannels, err := m.cfg.DB.FetchAllOpenChannels()
	if err != nil {
		return nil, err
	}

	// Filter out private channels.
	var channels []*channeldb.OpenChannel
	for _, c := range allChannels {
		// We'll skip any private channels, as they aren't used for
		// routing within the network by other nodes.
		if c.ChannelFlags&lnwire.FFAnnounceChannel == 0 {
			continue
		}

		channels = append(channels, c)
	}

	return channels, nil
}

// signAndSendNextUpdate computes and signs a valid update for the passed
// outpoint, with the ability to toggle the disabled bit. The new update will
// use the current time as the update's timestamp, or increment the old
// timestamp by 1 to ensure the update can propagate. If signing is successful,
// the new update will be sent out on the network.
func (m *ChanStatusManager) signAndSendNextUpdate(outpoint wire.OutPoint,
	disabled bool) error {

	// Retrieve the latest update for this channel. We'll use this
	// as our starting point to send the new update.
	chanUpdate, private, err := m.fetchLastChanUpdateByOutPoint(outpoint)
	if err != nil {
		return err
	}

	err = SignChannelUpdate(
		m.cfg.MessageSigner, m.cfg.OurKeyLoc, chanUpdate,
		ChanUpdSetDisable(disabled), ChanUpdSetTimestamp,
	)
	if err != nil {
		return err
	}

	return m.cfg.ApplyChannelUpdate(chanUpdate, &outpoint, private)
}

// fetchLastChanUpdateByOutPoint fetches the latest policy for our direction of
// a channel, and crafts a new ChannelUpdate with this policy. Returns an error
// in case our ChannelEdgePolicy is not found in the database. Also returns if
// the channel is private by checking AuthProof for nil.
func (m *ChanStatusManager) fetchLastChanUpdateByOutPoint(op wire.OutPoint) (
	*lnwire.ChannelUpdate1, bool, error) {

	// Get the edge info and policies for this channel from the graph.
	info, edge1, edge2, err := m.cfg.Graph.FetchChannelEdgesByOutpoint(&op)
	if err != nil {
		return nil, false, err
	}

	update, err := ExtractChannelUpdate(
		m.ourPubKeyBytes, info, edge1, edge2,
	)
	return update, info.AuthProof == nil, err
}

// loadInitialChanState determines the initial ChannelState for a particular
// outpoint. The initial ChanStatus for a given outpoint will either be
// ChanStatusEnabled or ChanStatusDisabled, determined by inspecting the bits on
// the most recent announcement. An error is returned if the latest update could
// not be retrieved.
func (m *ChanStatusManager) loadInitialChanState(
	outpoint *wire.OutPoint) (ChannelState, error) {

	lastUpdate, _, err := m.fetchLastChanUpdateByOutPoint(*outpoint)
	if err != nil {
		return ChannelState{}, err
	}

	// Determine the channel's starting status by inspecting the disable bit
	// on last announcement we sent out.
	var initialStatus ChanStatus
	if lastUpdate.ChannelFlags&lnwire.ChanUpdateDisabled == 0 {
		initialStatus = ChanStatusEnabled
	} else {
		initialStatus = ChanStatusDisabled
	}

	return ChannelState{
		Status: initialStatus,
	}, nil
}

// getOrInitChanStatus retrieves the current ChannelState for a particular
// outpoint. If the chanStates map already contains an entry for the outpoint,
// the value in the map is returned. Otherwise, the outpoint's initial status is
// computed and updated in the chanStates map before being returned.
func (m *ChanStatusManager) getOrInitChanStatus(
	outpoint wire.OutPoint) (ChannelState, error) {

	// Return the current ChannelState from the chanStates map if it is
	// already known to the ChanStatusManager.
	if curState, ok := m.chanStates[outpoint]; ok {
		return curState, nil
	}

	// Otherwise, determine the initial state based on the last update we
	// sent for the outpoint.
	initialState, err := m.loadInitialChanState(&outpoint)
	if err != nil {
		return ChannelState{}, err
	}

	// Finally, store the initial state in the chanStates map. This will
	// serve as are up-to-date view of the outpoint's current status, in
	// addition to making the channel eligible for detecting inactivity.
	m.chanStates[outpoint] = initialState

	return initialState, nil
}
