package ooni

import (
	_ "embed" // because we embed a file
	"io/ioutil"
	"os"
	"os/signal"
	"sync/atomic"
	"syscall"

	"github.com/apex/log"
	"github.com/ooni/probe-cli/v3/cmd/ooniprobe/internal/config"
	"github.com/ooni/probe-cli/v3/cmd/ooniprobe/internal/database"
	"github.com/ooni/probe-cli/v3/cmd/ooniprobe/internal/enginex"
	"github.com/ooni/probe-cli/v3/cmd/ooniprobe/internal/utils"
	engine "github.com/ooni/probe-cli/v3/internal/engine"
	"github.com/pkg/errors"
	"upper.io/db.v3/lib/sqlbuilder"
)

// ProbeCLI is the OONI Probe CLI context.
type ProbeCLI interface {
	Config() *config.Config
	DB() sqlbuilder.Database
	IsBatch() bool
	Home() string
	TempDir() string
	NewProbeEngine() (ProbeEngine, error)
}

// ProbeEngine is an instance of the OONI Probe engine.
type ProbeEngine interface {
	Close() error
	MaybeLookupLocation() error
	ProbeASNString() string
	ProbeCC() string
	ProbeIP() string
	ProbeNetworkName() string
}

// Probe contains the ooniprobe CLI context.
type Probe struct {
	config  *config.Config
	db      sqlbuilder.Database
	isBatch bool

	home    string
	tempDir string

	dbPath     string
	configPath string

	// We need to use a int32 in order to use the atomic.AddInt32/LoadInt32
	// operations to ensure consistent reads of the variables. We do not use
	// a 64 bit integer here because that may lead to crashes with 32 bit
	// OSes as documented in https://golang.org/pkg/sync/atomic/#pkg-note-BUG.
	isTerminatedAtomicInt int32

	softwareName    string
	softwareVersion string
}

// SetIsBatch sets the value of isBatch.
func (p *Probe) SetIsBatch(v bool) {
	p.isBatch = v
}

// IsBatch returns whether we're running in batch mode.
func (p *Probe) IsBatch() bool {
	return p.isBatch
}

// Config returns the configuration
func (p *Probe) Config() *config.Config {
	return p.config
}

// DB returns the database we're using
func (p *Probe) DB() sqlbuilder.Database {
	return p.db
}

// Home returns the home directory.
func (p *Probe) Home() string {
	return p.home
}

// TempDir returns the temporary directory.
func (p *Probe) TempDir() string {
	return p.tempDir
}

// IsTerminated checks to see if the isTerminatedAtomicInt is set to a non zero
// value and therefore we have received the signal to shutdown the running test
func (p *Probe) IsTerminated() bool {
	i := atomic.LoadInt32(&p.isTerminatedAtomicInt)
	return i != 0
}

// Terminate interrupts the running context
func (p *Probe) Terminate() {
	atomic.AddInt32(&p.isTerminatedAtomicInt, 1)
}

// ListenForSignals will listen for SIGINT and SIGTERM. When it receives those
// signals it will set isTerminatedAtomicInt to non-zero, which will cleanly
// shutdown the test logic.
//
// TODO refactor this to use a cancellable context.Context instead of a bool
// flag, probably as part of: https://github.com/ooni/probe-cli/issues/45
func (p *Probe) ListenForSignals() {
	s := make(chan os.Signal, 1)
	signal.Notify(s, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
	go func() {
		<-s
		log.Info("caught a stop signal, shutting down cleanly")
		p.Terminate()
	}()
}

// MaybeListenForStdinClosed will treat any error on stdin just
// like SIGTERM if and only if
//
//     os.Getenv("OONI_STDIN_EOF_IMPLIES_SIGTERM") == "true"
//
// When this feature is enabled, a collateral effect is that we swallow
// whatever is passed to us on the standard input.
//
// See https://github.com/ooni/probe-cli/pull/111 for more info
// regarding the design of this functionality.
//
// TODO refactor this to use a cancellable context.Context instead of a bool
// flag, probably as part of: https://github.com/ooni/probe-cli/issues/45
func (p *Probe) MaybeListenForStdinClosed() {
	if os.Getenv("OONI_STDIN_EOF_IMPLIES_SIGTERM") != "true" {
		return
	}
	go func() {
		defer p.Terminate()
		defer log.Info("stdin closed, shutting down cleanly")
		b := make([]byte, 1<<10)
		for {
			if _, err := os.Stdin.Read(b); err != nil {
				return
			}
		}
	}()
}

// Init the OONI manager
func (p *Probe) Init(softwareName, softwareVersion string) error {
	var err error

	if err = MaybeInitializeHome(p.home); err != nil {
		return err
	}

	if p.configPath != "" {
		log.Debugf("Reading config file from %s", p.configPath)
		p.config, err = config.ReadConfig(p.configPath)
	} else {
		log.Debug("Reading default config file")
		p.config, err = InitDefaultConfig(p.home)
	}
	if err != nil {
		return err
	}
	if err = p.config.MaybeMigrate(); err != nil {
		return errors.Wrap(err, "migrating config")
	}

	p.dbPath = utils.DBDir(p.home, "main")
	log.Debugf("Connecting to database sqlite3://%s", p.dbPath)
	db, err := database.Connect(p.dbPath)
	if err != nil {
		return err
	}
	p.db = db

	tempDir, err := ioutil.TempDir("", "ooni")
	if err != nil {
		return errors.Wrap(err, "creating TempDir")
	}
	p.tempDir = tempDir

	p.softwareName = softwareName
	p.softwareVersion = softwareVersion
	return nil
}

// NewSession creates a new ooni/probe-engine session using the
// current configuration inside the context. The caller must close
// the session when done using it, by calling sess.Close().
func (p *Probe) NewSession() (*engine.Session, error) {
	kvstore, err := engine.NewFileSystemKVStore(
		utils.EngineDir(p.home),
	)
	if err != nil {
		return nil, errors.Wrap(err, "creating engine's kvstore")
	}
	return engine.NewSession(engine.SessionConfig{
		AssetsDir:       utils.AssetsDir(p.home),
		KVStore:         kvstore,
		Logger:          enginex.Logger,
		SoftwareName:    p.softwareName,
		SoftwareVersion: p.softwareVersion,
		TempDir:         p.tempDir,
	})
}

// NewProbeEngine creates a new ProbeEngine instance.
func (p *Probe) NewProbeEngine() (ProbeEngine, error) {
	sess, err := p.NewSession()
	if err != nil {
		return nil, err
	}
	return sess, nil
}

// NewProbe creates a new probe instance.
func NewProbe(configPath string, homePath string) *Probe {
	return &Probe{
		home:                  homePath,
		config:                &config.Config{},
		configPath:            configPath,
		isTerminatedAtomicInt: 0,
	}
}

// MaybeInitializeHome does the setup for a new OONI Home
func MaybeInitializeHome(home string) error {
	for _, d := range utils.RequiredDirs(home) {
		if _, e := os.Stat(d); e != nil {
			if err := os.MkdirAll(d, 0700); err != nil {
				return err
			}
		}
	}
	return nil
}

//go:embed default-config.json
var defaultConfig []byte

// InitDefaultConfig reads the config from common locations or creates it if
// missing.
func InitDefaultConfig(home string) (*config.Config, error) {
	var (
		err        error
		c          *config.Config
		configPath = utils.ConfigPath(home)
	)

	c, err = config.ReadConfig(configPath)
	if err != nil {
		if os.IsNotExist(err) {
			log.Debugf("writing default config to %s", configPath)
			if err = ioutil.WriteFile(configPath, defaultConfig, 0644); err != nil {
				return nil, err
			}
			// If the user did the informed consent procedure in
			// probe-legacy, migrate it over.
			if utils.DidLegacyInformedConsent() {
				c, err := config.ReadConfig(configPath)
				if err != nil {
					return nil, err
				}
				c.Lock()
				c.InformedConsent = true
				c.Unlock()
				if err := c.Write(); err != nil {
					return nil, err
				}
			}

			return InitDefaultConfig(home)
		}
		return nil, err
	}

	return c, nil
}