//nolint:exhaustivestruct,gochecknoglobals
package prepare

//nolint:golint
import (
	_ "embed"
	"fmt"
	"io/ioutil"
	"os"
	"os/exec"
	"os/user"
	"strings"
	"text/template"

	"github.com/rs/zerolog"
	"github.com/rs/zerolog/log"
	"github.com/spf13/cobra"
)

const (
	mopidyGroup        = "audio"
	mopidyUser         = "mopidy"
	serviceFileName    = "schnutibox.service"
	serviceLocation    = "/etc/systemd/system"
	timesyncGroup      = "systemd-timesync"
	timesyncUser       = "systemd-timesync"
	schnutiboxUser     = "schnutibox"
	schnutboxConfigDir = "/etc/schnutibox"
	upmpdcliUser       = "upmpdcli"
	upmpdcliGroup      = "nogroup"
)

//go:embed assets/files/schnutibox.service
var serviceFile []byte

//go:embed assets/templates/fstab.raspbian.tmpl
var fstabRaspbianTemplate string

//go:embed assets/templates/mopidy.conf.tmpl
var mopidyConf string

//go:embed assets/files/upmpdcli.conf
var upmpdcliConf []byte

//go:embed assets/files/ntp.service
var ntpService []byte

// Config.
var Cfg = struct {
	ReadOnly            bool
	System              string
	Spotify             bool
	SpotifyUsername     string
	SpotifyPassword     string
	SpotifyClientID     string
	SpotifyClientSecret string
}{}

// BoxService creates a systemd service for schnutibox.
func BoxService(filename string, enable bool) error {
	logger := log.With().Str("stage", "BoxService").Logger()

	if err := CreateUser(); err != nil {
		return fmt.Errorf("could not create user: %w", err)
	}

	// Create config dir.
	if err := os.MkdirAll(schnutboxConfigDir, os.ModePerm); err != nil {
		return fmt.Errorf("could not create config dir: %w", err)
	}

	//nolint:gosec
	if err := ioutil.WriteFile(filename, serviceFile, 0o644); err != nil {
		return fmt.Errorf("could not write service file: %w", err)
	}

	if enable {
		cmd := exec.Command("systemctl", "daemon-reload")
		logger.Info().Str("cmd", cmd.String()).Msg("running")

		if err := cmd.Run(); err != nil {
			return fmt.Errorf("could not reload service files: %w", err)
		}

		cmd = exec.Command("systemctl", "enable", "schnutibox.service")
		logger.Info().Str("cmd", cmd.String()).Msg("running")

		if err := cmd.Run(); err != nil {
			return fmt.Errorf("could not enable service: %w", err)
		}
	}

	return nil
}

func NTP() error {
	logger := log.With().Str("stage", "NTP").Logger()

	cmd := exec.Command("apt-get", "install", "-y", "ntp", "ntpdate")
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

	logger.Info().Str("cmd", cmd.String()).Msg("running")

	if err := cmd.Run(); err != nil {
		return fmt.Errorf("could not install ntp: %w", err)
	}

	if err := ioutil.WriteFile("/etc/systemd/system/ntp.service", ntpService, 0o644); err != nil {
		return fmt.Errorf("could not copy ntp service file: %w", err)
	}

	cmd = exec.Command("systemctl", "daemon-reload")
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

	logger.Info().Str("cmd", cmd.String()).Msg("running")

	if err := cmd.Run(); err != nil {
		return fmt.Errorf("could not reload systemd service files: %w", err)
	}

	return nil
}

// Fstab creates a fstab for a read-only system.
// nolint:funlen
func Fstab(system string) error {
	logger := log.With().Str("stage", "Fstab").Logger()

	// Getting timesync user and group informations.
	timesyncUser, err := user.Lookup(timesyncUser)
	if err != nil {
		return fmt.Errorf("could not lookup timesync user: %w", err)
	}

	timesyncGroup, err := user.LookupGroup(timesyncGroup)
	if err != nil {
		return fmt.Errorf("could not lookup timesync group: %w", err)
	}

	logger.Debug().Str("uid", timesyncUser.Uid).Str("gid", timesyncGroup.Gid).Msg("timesyncd")

	// Getting mopidy user and group informations.
	mopidyUser, err := user.Lookup(mopidyUser)
	if err != nil {
		return fmt.Errorf("could not lookup mopidy user: %w", err)
	}

	mopidyGroup, err := user.LookupGroup(mopidyGroup)
	if err != nil {
		return fmt.Errorf("could not lookup mopidy group: %w", err)
	}

	logger.Debug().Str("uid", mopidyUser.Uid).Str("gid", mopidyGroup.Gid).Msg("mopidy")

	// Getting upmpd user and group informations.
	upmpdcliUser, err := user.Lookup(upmpdcliUser)
	if err != nil {
		return fmt.Errorf("could not lookup upmpdcli user: %w", err)
	}

	upmpdcliGroup, err := user.LookupGroup(upmpdcliGroup)
	if err != nil {
		return fmt.Errorf("could not lookup upmpdcli group: %w", err)
	}

	logger.Debug().Str("uid", upmpdcliUser.Uid).Str("gid", upmpdcliGroup.Gid).Msg("upmpdcli")

	// Chose the right template.
	// In future it should be a switch statement.
	tmpl := fstabRaspbianTemplate

	// Parse template.
	t := template.Must(template.New("fstab").Parse(tmpl))

	// Open fstab.
	f, err := os.Create("/etc/fstab")
	if err != nil {
		return fmt.Errorf("could not create file to write: %w", err)
	}
	defer f.Close()

	// Create and write.
	if err := t.Execute(f, struct {
		TimesyncUID string
		TimesyncGID string
		MopidyUID   string
		MopidyGID   string
		UpmpdcliUID string
		UpmpdcliGID string
	}{
		timesyncUser.Uid,
		timesyncGroup.Gid,
		mopidyUser.Uid,
		mopidyGroup.Gid,
		upmpdcliUser.Uid,
		upmpdcliGroup.Gid,
	}); err != nil {
		return fmt.Errorf("could not write templated fstab: %w", err)
	}

	return nil
}

// RemovePkgs removes not needed software in read-only mode.
func RemovePkgs(system string) error {
	logger := log.With().Str("stage", "RemovePkgs").Logger()
	if system != "raspbian" {
		logger.Info().Msg("nothing to do")

		return nil
	}

	pkgs := []string{
		"cron",
		"logrotate",
		"triggerhappy",
		"dphys-swapfile",
		"fake-hwclock",
		"samba-common",
	}

	for _, i := range pkgs {
		logger.Debug().Str("pkg", i).Msg("remove package")
		cmd := exec.Command("apt-get", "remove", "-y", i)
		logger.Debug().Str("cmd", cmd.String()).Msg("running")

		if err := cmd.Run(); err != nil {
			return fmt.Errorf("could not remove pkg: %w", err)
		}
	}

	cmd := exec.Command("apt-get", "autoremove", "--purge", "-y")
	logger.Debug().Str("cmd", cmd.String()).Msg("running")

	if err := cmd.Run(); err != nil {
		return fmt.Errorf("could not do an autoremove: %w", err)
	}

	return nil
}

// CreateUser creates schnutibox system user and group.
func CreateUser() error {
	logger := log.With().Str("stage", "CreateUser").Logger()

	cmd := exec.Command("adduser", "--system", "--group", "--no-create-home", schnutiboxUser)
	logger.Debug().Str("cmd", cmd.String()).Msg("running")

	if err := cmd.Run(); err != nil {
		return fmt.Errorf("could not create user: %w", err)
	}

	return nil
}

// CreateSymlinks creates all needed symlinks.
func CreateSymlinks(system string) error {
	logger := log.With().Str("stage", "Symlinks").Logger()

	links := []struct {
		symlink string
		dest    string
	}{
		{
			"/var/lib/dhcp",
			"/tmp/dhcp",
		},
		{
			"/var/spool",
			"/tmp/spool",
		},
		{
			"/var/lock",
			"/tmp/lock",
		},
		{
			"/etc/resolv.conf",
			"/tmp/resolv.conf",
		},
	}

	removeFiles := []string{
		"/var/lib/dhcp",
		"/var/spool",
		"/var/lock",
		"/etc/resolv.conf",
	}

	for _, i := range removeFiles {
		logger.Debug().Str("item", i).Msg("remove file/directory")

		if err := os.RemoveAll(i); err != nil {
			return err
		}
	}

	for _, i := range links {
		logger.Debug().Str("symlink", i.symlink).Str("dest", i.dest).Msg("linking")

		dest, err := os.Readlink(i.symlink)
		if err == nil {
			if dest == i.dest {
				logger.Debug().Str("dest", dest).Str("expected dest", i.dest).Msg("matches")

				continue
			}
		}

		if err := os.Symlink(i.dest, i.symlink); err != nil {
			return fmt.Errorf("could not create symlink: %w", err)
		}
	}

	return nil
}

// cmdlineTxt modifies the /boot/cmdline.txt.
func cmdlineTxt(system string) error {
	// Read.
	oldLine, err := ioutil.ReadFile("/boot/cmdline.txt")
	if err != nil {
		return fmt.Errorf("could not read cmdline.txt: %w", err)
	}

	newLine := strings.TrimSuffix(string(oldLine), "\n") + " " + "fastboot" + " " + "noswap"

	// Write.
	if err := ioutil.WriteFile("/boot/cmdline.txt", []byte(newLine), 0o644); err != nil {
		return fmt.Errorf("could not write cmdline.txt: %w", err)
	}

	return nil
}

// makeReadOnly executes stuff if a read-only system is wanted.
func makeReadOnly(system string) error {
	if err := RemovePkgs(system); err != nil {
		return fmt.Errorf("could not remove pkgs: %w", err)
	}

	if err := CreateSymlinks(system); err != nil {
		return fmt.Errorf("could not create symlinks: %w", err)
	}

	if err := Fstab(system); err != nil {
		return fmt.Errorf("could not create fstab: %w", err)
	}

	if err := cmdlineTxt(system); err != nil {
		return fmt.Errorf("could not modify cmdline.txt: %w", err)
	}

	return nil
}

// Mopidy setups mopidy.
//nolint:funlen
func Mopidy() error {
	logger := log.With().Str("stage", "Mopidy").Logger()

	// GPG Key.
	cmd := exec.Command("/bin/sh", "-c", "wget -q -O - https://apt.mopidy.com/mopidy.gpg | apt-key add -")
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

	logger.Debug().Str("cmd", cmd.String()).Msg("running")

	if err := cmd.Run(); err != nil {
		return fmt.Errorf("could not add mopidy key: %w", err)
	}

	// Repo.
	cmd = exec.Command(
		"/bin/sh", "-c",
		"wget -q -O /etc/apt/sources.list.d/mopidy.list https://apt.mopidy.com/buster.list",
	)
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

	logger.Debug().Str("cmd", cmd.String()).Msg("running")

	if err := cmd.Run(); err != nil {
		return fmt.Errorf("could not download apt repo: %w", err)
	}

	// Update.
	cmd = exec.Command("apt-get", "update")
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

	logger.Debug().Str("cmd", cmd.String()).Msg("running")

	if err := cmd.Run(); err != nil {
		return fmt.Errorf("could not update apt: %w", err)
	}

	// Install.
	cmd = exec.Command(
		"apt-get", "install", "-y",
		"mopidy",
		"mopidy-alsamixer",
		"mopidy-mpd",
		"mopidy-spotify",
	)
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

	logger.Debug().Str("cmd", cmd.String()).Msg("running")

	if err := cmd.Run(); err != nil {
		return fmt.Errorf("could not install mopidy: %w", err)
	}

	// Enable service.
	cmd = exec.Command("systemctl", "enable", "mopidy.service")
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

	logger.Debug().Str("cmd", cmd.String()).Msg("running")

	if err := cmd.Run(); err != nil {
		return fmt.Errorf("could not enable mopidy service: %w", err)
	}

	// Config.
	if Cfg.SpotifyUsername != "" &&
		Cfg.SpotifyPassword != "" &&
		Cfg.SpotifyClientID != "" &&
		Cfg.SpotifyClientSecret != "" {
		Cfg.Spotify = true
	}

	t := template.Must(template.New("mopidyConf").Parse(mopidyConf))

	f, err := os.Create("/etc/mopidy/mopidy.conf")
	if err != nil {
		return fmt.Errorf("could not create file to write: %w", err)
	}
	defer f.Close()

	if err := t.Execute(f, Cfg); err != nil {
		return fmt.Errorf("could not write compiled mopidy config: %w", err)
	}

	return nil
}

// Upmpdcli setups upmpdcli.
//nolint:funlen
func Upmpdcli() error {
	logger := log.With().Str("stage", "Upmpdcli").Logger()

	// GPG Key.
	cmd := exec.Command(
		"/bin/sh", "-c",
		"wget https://www.lesbonscomptes.com/pages/lesbonscomptes.gpg -O /usr/share/keyrings/lesbonscomptes.gpg",
	)
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

	logger.Debug().Str("cmd", cmd.String()).Msg("running")

	if err := cmd.Run(); err != nil {
		return fmt.Errorf("could not add upmpdcli key: %w", err)
	}

	// Repo.
	cmd = exec.Command(
		"curl",
		"-o",
		"/etc/apt/sources.list.d/upmpdcli.list",
		"https://www.lesbonscomptes.com/upmpdcli/pages/upmpdcli-rbuster.list",
	)
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

	logger.Debug().Str("cmd", cmd.String()).Msg("running")

	if err := cmd.Run(); err != nil {
		return fmt.Errorf("could not add sources list for upmpdcli: %w", err)
	}

	// Update.
	cmd = exec.Command("apt-get", "update")
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

	logger.Debug().Str("cmd", cmd.String()).Msg("running")

	if err := cmd.Run(); err != nil {
		return fmt.Errorf("could not update apt: %w", err)
	}

	// Install.
	cmd = exec.Command(
		"apt-get", "install", "-y",
		"upmpdcli",
	)
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

	logger.Debug().Str("cmd", cmd.String()).Msg("running")

	if err := cmd.Run(); err != nil {
		return fmt.Errorf("could not install mopidy: %w", err)
	}

	// Enable service.
	cmd = exec.Command("systemctl", "enable", "upmpdcli.service")
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

	logger.Debug().Str("cmd", cmd.String()).Msg("running")

	if err := cmd.Run(); err != nil {
		return fmt.Errorf("could not enable upmpdcli service: %w", err)
	}

	// Create config.
	if err := ioutil.WriteFile("/etc/upmpdcli.conf", upmpdcliConf, 0o644); err != nil {
		return fmt.Errorf("could not copy upmpdcli config: %w", err)
	}

	return nil
}

func Run(cmd *cobra.Command, args []string) {
	log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})

	// Running the preparation.
	if err := BoxService(serviceLocation+"/"+serviceFileName, true); err != nil {
		log.Fatal().Err(err).Msg("could not create schnutibox service")
	}

	// Setup NTP.
	if err := NTP(); err != nil {
		log.Fatal().Err(err).Msg("could not setup ntp")
	}

	// Setup mopidy.
	if err := Mopidy(); err != nil {
		log.Fatal().Err(err).Msg("could not setup mopidy")
	}

	// Setup upmpdcli.
	if err := Upmpdcli(); err != nil {
		log.Fatal().Err(err).Msg("could not setup upmpdcli")
	}

	// Making system read-only.
	if Cfg.ReadOnly {
		if err := makeReadOnly(Cfg.System); err != nil {
			log.Fatal().Err(err).Msg("could not make system read-only")
		}
	}
}
