// -*- Mode: Go; indent-tabs-mode: t -*-

/*
 * Copyright (C) 2015-2020 Canonical Ltd
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3 as
 * published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

package main

import (
	"context"
	"errors"
	"fmt"
	"os"
	"os/signal"
	"syscall"
	"time"

	"github.com/snapcore/snapd/daemon"
	"github.com/snapcore/snapd/logger"
	"github.com/snapcore/snapd/osutil"
	"github.com/snapcore/snapd/sandbox"
	"github.com/snapcore/snapd/secboot"
	"github.com/snapcore/snapd/seclog"
	"github.com/snapcore/snapd/snapdenv"
	"github.com/snapcore/snapd/snapdtool"
	"github.com/snapcore/snapd/syscheck"
	"github.com/snapcore/snapd/systemd"
)

var (
	syscheckCheckSystem = syscheck.CheckSystem
	openAuditWriter     = seclog.OpenAuditWriter
	newSlogLogger       = seclog.NewSlogLogger
)

const (
	secLogAppID                 = "canonical.snapd.snapd"
	secLogMinLevel seclog.Level = seclog.LevelInfo
)

func init() {
	logger.SimpleSetup(nil)
}

func main() {
	// When preseeding re-exec is not used
	if snapdenv.Preseeding() {
		logger.Noticef("running for preseeding")
	} else {
		snapdtool.ExecInSnapdOrCoreSnap()
	}

	// Set up security logging via the audit subsystem.
	teardownSecurityLogging := setupSecurityLogging()

	secboot.HijackAndRunArgon2OutOfProcessHandlerOnArg([]string{"argon2-proc"})

	snapdtool.MaybeCompleteFIPSSetup()

	// TODO look into signal.NotifyContext
	ch := make(chan os.Signal, 2)
	signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
	err := run(ch)

	// Tear down security logging explicitly so that the "disabled" log
	// entry is written before any os.Exit call
	teardownSecurityLogging()

	if err != nil {
		if errors.Is(err, daemon.ErrRestartSocket) {
			// Note that we don't prepend: "error: " here because
			// ErrRestartSocket is not an error as such.
			fmt.Fprintln(os.Stdout, err)
			// the exit code must be in sync with
			// data/systemd/snapd.service.in:SuccessExitStatus=
			os.Exit(42)
		} else if errors.Is(err, daemon.ErrNoFailureRecoveryNeeded) {
			// Similar consideration as above.
			fmt.Fprintln(os.Stdout, err)
			// We were invoked from a failure handler, but there is
			// nothing to recover from in the state, as such the
			// failure handling was successful.
			return
		}
		fmt.Fprintf(os.Stderr, "cannot run daemon: %v\n", err)
		os.Exit(1)
	}
}

func setupSecurityLogging() (teardown func()) {
	auditWriter, err := openAuditWriter()
	if err != nil {
		logger.Noticef("cannot set up security logger: %v", err)
		return func() {}
	}
	sl := newSlogLogger(auditWriter, secLogAppID, secLogMinLevel)
	seclog.Setup(sl)
	seclog.LogLoggerEnabled()
	return func() {
		seclog.LogLoggerDisabled()
		auditWriter.Close()
	}
}

func runWatchdog(d *daemon.Daemon) (*time.Ticker, error) {
	// not running under systemd
	if os.Getenv("WATCHDOG_USEC") == "" {
		return nil, nil
	}
	usec := osutil.GetenvInt64("WATCHDOG_USEC")
	if usec == 0 {
		return nil, fmt.Errorf("cannot parse WATCHDOG_USEC: %q", os.Getenv("WATCHDOG_USEC"))
	}
	dur := time.Duration(usec/2) * time.Microsecond
	logger.Debugf("Setting up sd_notify() watchdog timer every %s", dur)
	wt := time.NewTicker(dur)

	go func() {
		for {
			select {
			case <-wt.C:
				// TODO: poke the snapd API here and
				//       only report WATCHDOG=1 if it
				//       replies with valid data
				systemd.SdNotify("WATCHDOG=1")
			case <-d.Dying():
				return
			}
		}
	}()

	return wt, nil
}

var checkRunningConditionsRetryDelay = 300 * time.Second

func run(ch chan os.Signal) error {
	ctx := context.Background()

	t0 := time.Now().Truncate(time.Millisecond)
	snapdenv.SetUserAgentFromVersion(snapdtool.Version, sandbox.ForceDevMode)

	d, err := daemon.New()
	if err != nil {
		return err
	}
	if err := d.Init(); err != nil {
		return err
	}

	// Run syscheck check now, if anything goes wrong with the
	// check we go into "degraded" mode where we always report
	// the given error to any snap client.
	var checkTicker <-chan time.Time
	var tic *time.Ticker
	if err := syscheckCheckSystem(); err != nil {
		degradedErr := fmt.Errorf("system does not fully support snapd: %s", err)
		logger.Noticef("%s", degradedErr)
		d.SetDegradedMode(degradedErr)
		tic = time.NewTicker(checkRunningConditionsRetryDelay)
		checkTicker = tic.C
	}

	d.Version = snapdtool.Version

	if err := d.Start(ctx); err != nil {
		return err
	}

	watchdog, err := runWatchdog(d)
	if err != nil {
		return fmt.Errorf("cannot run software watchdog: %v", err)
	}
	if watchdog != nil {
		defer watchdog.Stop()
	}

	logger.Debugf("activation done in %v", time.Now().Truncate(time.Millisecond).Sub(t0))

out:
	for {
		select {
		case sig := <-ch:
			logger.Noticef("Exiting on %s signal.\n", sig)
			break out
		case <-d.Dying():
			// something called Stop()
			break out
		case <-checkTicker:
			if err := syscheckCheckSystem(); err == nil {
				d.SetDegradedMode(nil)
				tic.Stop()
			}
		}
	}

	return d.Stop(ch)
}
