package watchtower

import (
	"net"
	"sync/atomic"

	"github.com/btcsuite/btcd/btcec"
	"github.com/lightningnetwork/lnd/brontide"
	"github.com/lightningnetwork/lnd/tor"
	"github.com/lightningnetwork/lnd/watchtower/lookout"
	"github.com/lightningnetwork/lnd/watchtower/wtserver"
)

// Standalone encapsulates the server-side functionality required by watchtower
// clients. A Standalone couples the two primary subsystems such that, as a
// unit, this instance can negotiate sessions with clients, accept state updates
// for active sessions, monitor the chain for breaches matching known breach
// hints, publish reconstructed justice transactions on behalf of tower clients.
type Standalone struct {
	started uint32 // to be used atomically
	stopped uint32 // to be used atomically

	cfg *Config

	// listeners is a reference to the wtserver's listeners.
	listeners []net.Listener

	// server is the client endpoint, used for negotiating sessions and
	// uploading state updates.
	server wtserver.Interface

	// lookout is a service that monitors the chain and inspects the
	// transactions found in new blocks against the state updates received
	// by the server.
	lookout lookout.Service
}

// New validates the passed Config and returns a fresh Standalone instance if
// the tower's subsystems could be properly initialized.
func New(cfg *Config) (*Standalone, error) {
	// The tower must have listening address in order to accept new updates
	// from clients.
	if len(cfg.ListenAddrs) == 0 {
		return nil, ErrNoListeners
	}

	// Assign the default read timeout if none is provided.
	if cfg.ReadTimeout == 0 {
		cfg.ReadTimeout = DefaultReadTimeout
	}

	// Assign the default write timeout if none is provided.
	if cfg.WriteTimeout == 0 {
		cfg.WriteTimeout = DefaultWriteTimeout
	}

	punisher := lookout.NewBreachPunisher(&lookout.PunisherConfig{
		PublishTx: cfg.PublishTx,
	})

	// Initialize the lookout service with its required resources.
	lookout := lookout.New(&lookout.Config{
		BlockFetcher:   cfg.BlockFetcher,
		DB:             cfg.DB,
		EpochRegistrar: cfg.EpochRegistrar,
		Punisher:       punisher,
	})

	// Create a brontide listener on each of the provided listening
	// addresses. Client should be able to connect to any of open ports to
	// communicate with this Standalone instance.
	listeners := make([]net.Listener, 0, len(cfg.ListenAddrs))
	for _, listenAddr := range cfg.ListenAddrs {
		listener, err := brontide.NewListener(
			cfg.NodeKeyECDH, listenAddr.String(),
		)
		if err != nil {
			return nil, err
		}

		listeners = append(listeners, listener)
	}

	// Initialize the server with its required resources.
	server, err := wtserver.New(&wtserver.Config{
		ChainHash:     cfg.ChainHash,
		DB:            cfg.DB,
		NodeKeyECDH:   cfg.NodeKeyECDH,
		Listeners:     listeners,
		ReadTimeout:   cfg.ReadTimeout,
		WriteTimeout:  cfg.WriteTimeout,
		NewAddress:    cfg.NewAddress,
		DisableReward: true,
	})
	if err != nil {
		return nil, err
	}

	return &Standalone{
		cfg:       cfg,
		listeners: listeners,
		server:    server,
		lookout:   lookout,
	}, nil
}

// Start idempotently starts the Standalone, an error is returned if the
// subsystems could not be initialized.
func (w *Standalone) Start() error {
	if !atomic.CompareAndSwapUint32(&w.started, 0, 1) {
		return nil
	}

	log.Infof("Starting watchtower")

	// If a tor controller exists in the config, then automatically create a
	// hidden service for the watchtower to accept inbound connections from.
	if w.cfg.TorController != nil {
		log.Infof("Creating watchtower hidden service")
		if err := w.createNewHiddenService(); err != nil {
			return err
		}
	}

	if err := w.lookout.Start(); err != nil {
		return err
	}
	if err := w.server.Start(); err != nil {
		w.lookout.Stop()
		return err
	}

	log.Infof("Watchtower started successfully")

	return nil
}

// Stop idempotently stops the Standalone and blocks until the subsystems have
// completed their shutdown.
func (w *Standalone) Stop() error {
	if !atomic.CompareAndSwapUint32(&w.stopped, 0, 1) {
		return nil
	}

	log.Infof("Stopping watchtower")

	w.server.Stop()
	w.lookout.Stop()

	log.Infof("Watchtower stopped successfully")

	return nil
}

// createNewHiddenService automatically sets up a v2 or v3 onion service in
// order to listen for inbound connections over Tor.
func (w *Standalone) createNewHiddenService() error {
	// Get all the ports the watchtower is listening on. These will be used to
	// map the hidden service's virtual port.
	listenPorts := make([]int, 0, len(w.listeners))
	for _, listener := range w.listeners {
		port := listener.Addr().(*net.TCPAddr).Port
		listenPorts = append(listenPorts, port)
	}

	// Once we've created the port mapping, we can automatically create the
	// hidden service. The service's private key will be saved on disk in order
	// to persistently have access to this hidden service across restarts.
	onionCfg := tor.AddOnionConfig{
		VirtualPort: DefaultPeerPort,
		TargetPorts: listenPorts,
		Store:       tor.NewOnionFile(w.cfg.WatchtowerKeyPath, 0600),
		Type:        w.cfg.Type,
	}

	addr, err := w.cfg.TorController.AddOnion(onionCfg)
	if err != nil {
		return err
	}

	// Append this address to ExternalIPs so that it will be exposed in
	// tower info calls.
	w.cfg.ExternalIPs = append(w.cfg.ExternalIPs, addr)

	return nil
}

// PubKey returns the public key for the watchtower used to authentication and
// encrypt traffic with clients.
//
// NOTE: Part of the watchtowerrpc.WatchtowerBackend interface.
func (w *Standalone) PubKey() *btcec.PublicKey {
	return w.cfg.NodeKeyECDH.PubKey()
}

// ListeningAddrs returns the listening addresses where the watchtower server
// can accept client connections.
//
// NOTE: Part of the watchtowerrpc.WatchtowerBackend interface.
func (w *Standalone) ListeningAddrs() []net.Addr {
	addrs := make([]net.Addr, 0, len(w.listeners))
	for _, listener := range w.listeners {
		addrs = append(addrs, listener.Addr())
	}

	return addrs
}

// ExternalIPs returns the addresses where the watchtower can be reached by
// clients externally.
//
// NOTE: Part of the watchtowerrpc.WatchtowerBackend interface.
func (w *Standalone) ExternalIPs() []net.Addr {
	addrs := make([]net.Addr, 0, len(w.cfg.ExternalIPs))
	addrs = append(addrs, w.cfg.ExternalIPs...)

	return addrs
}