package privval

import (
	"net"
	"time"

	cmn "github.com/tendermint/tendermint/libs/common"
	"github.com/tendermint/tendermint/libs/log"
	"github.com/tendermint/tendermint/types"
)

// IPCValOption sets an optional parameter on the SocketPV.
type IPCValOption func(*IPCVal)

// IPCValConnTimeout sets the read and write timeout for connections
// from external signing processes.
func IPCValConnTimeout(timeout time.Duration) IPCValOption {
	return func(sc *IPCVal) { sc.connTimeout = timeout }
}

// IPCValHeartbeat sets the period on which to check the liveness of the
// connected Signer connections.
func IPCValHeartbeat(period time.Duration) IPCValOption {
	return func(sc *IPCVal) { sc.connHeartbeat = period }
}

// IPCVal implements PrivValidator, it uses a unix socket to request signatures
// from an external process.
type IPCVal struct {
	cmn.BaseService
	*RemoteSignerClient

	addr string

	connTimeout   time.Duration
	connHeartbeat time.Duration

	conn       net.Conn
	cancelPing chan struct{}
	pingTicker *time.Ticker
}

// Check that IPCVal implements PrivValidator.
var _ types.PrivValidator = (*IPCVal)(nil)

// NewIPCVal returns an instance of IPCVal.
func NewIPCVal(
	logger log.Logger,
	socketAddr string,
) *IPCVal {
	sc := &IPCVal{
		addr:          socketAddr,
		connTimeout:   connTimeout,
		connHeartbeat: connHeartbeat,
	}

	sc.BaseService = *cmn.NewBaseService(logger, "IPCVal", sc)

	return sc
}

// OnStart implements cmn.Service.
func (sc *IPCVal) OnStart() error {
	err := sc.connect()
	if err != nil {
		sc.Logger.Error("OnStart", "err", err)
		return err
	}

	sc.RemoteSignerClient, err = NewRemoteSignerClient(sc.conn)
	if err != nil {
		return err
	}

	// Start a routine to keep the connection alive
	sc.cancelPing = make(chan struct{}, 1)
	sc.pingTicker = time.NewTicker(sc.connHeartbeat)
	go func() {
		for {
			select {
			case <-sc.pingTicker.C:
				err := sc.Ping()
				if err != nil {
					sc.Logger.Error("Ping", "err", err)
				}
			case <-sc.cancelPing:
				sc.pingTicker.Stop()
				return
			}
		}
	}()

	return nil
}

// OnStop implements cmn.Service.
func (sc *IPCVal) OnStop() {
	if sc.cancelPing != nil {
		close(sc.cancelPing)
	}

	if sc.conn != nil {
		if err := sc.conn.Close(); err != nil {
			sc.Logger.Error("OnStop", "err", err)
		}
	}
}

func (sc *IPCVal) connect() error {
	la, err := net.ResolveUnixAddr("unix", sc.addr)
	if err != nil {
		return err
	}

	conn, err := net.DialUnix("unix", nil, la)
	if err != nil {
		return err
	}

	sc.conn = newTimeoutConn(conn, sc.connTimeout)

	return nil
}
