//nolint:exhaustivestruct,gochecknoglobals
package prepare

import (
	"fmt"
	"io/ioutil"
	"os"
	"os/exec"
	"os/user"
	"strings"
	"text/template"

	"github.com/rs/zerolog"
	"github.com/rs/zerolog/log"
	"github.com/spf13/cobra"
	assets "go.xsfx.dev/schnutibox/assets/prepare"
)

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"
	snapserverUser     = "snapserver"
	snapserverGroup    = "snapserver"
	snapclientUser     = "snapclient"
	snapclientGroup    = "snapclient"
)

// Cfg represents the structured data for the schnutibox config file.
var Cfg = struct {
	RFIDReader          string
	ReadOnly            bool
	Spotify             bool
	SpotifyClientID     string
	SpotifyClientSecret string
	SpotifyPassword     string
	SpotifyUsername     string
	StopID              string
	System              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)
	}

	schnutiboxService, err := assets.Files.ReadFile("files/schnutibox.service")
	if err != nil {
		return fmt.Errorf("could not get service file: %w", err)
	}

	//nolint:gosec,gomnd
	if err := ioutil.WriteFile(filename, schnutiboxService, 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)
	}

	ntpService, err := assets.Files.ReadFile("files/ntp.service")
	if err != nil {
		return fmt.Errorf("could not get ntp service file: %w", err)
	}

	// nolint:gosec,gomnd
	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,cyclop
func fstab(system string) error {
	logger := log.With().Str("stage", "Fstab").Logger()
	logger.Debug().Str("system", system).Msg("ignoring for now")

	// 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")

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

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

	logger.Debug().Str("uid", snapserverUser.Uid).Str("gid", snapserverGroup.Gid).Msg("snapserver")

	snapclientUser, err := user.Lookup(snapclientUser)
	if err != nil {
		return fmt.Errorf("could not lookup snapclient user: %w", err)
	}

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

	logger.Debug().Str("uid", snapclientUser.Uid).Str("gid", snapclientGroup.Gid).Msg("snapclient")

	// Chose the right template.
	// In future it should be a switch statement.
	tmpl, err := assets.Templates.ReadFile("templates/fstab.raspbian.tmpl")
	if err != nil {
		return fmt.Errorf("could not get fstab template: %w", err)
	}

	// Parse template.
	t := template.Must(template.New("fstab").Parse(string(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
		SnapserverUID string
		SnapserverGID string
		SnapclientUID string
		SnapclientGID string
	}{
		timesyncUser.Uid,
		timesyncGroup.Gid,
		mopidyUser.Uid,
		mopidyGroup.Gid,
		upmpdcliUser.Uid,
		upmpdcliGroup.Gid,
		snapserverUser.Uid,
		snapserverGroup.Gid,
		snapclientUser.Uid,
		snapclientGroup.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
}

func udevRules() error {
	logger := log.With().Str("stage", "CreateUDEVrules").Logger()
	logger.Info().Msg("writing udev rule file")

	// Parse template.
	tmpl, err := assets.Templates.ReadFile("templates/50-neuftech.rules.tmpl")
	if err != nil {
		return fmt.Errorf("could not get udev rules file: %w", err)
	}

	t := template.Must(template.New("udev").Parse(string(tmpl)))

	// Open file.
	f, err := os.Create("/etc/udev/rules.d/50-neuftech.rules")
	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 {
		SchnutiboxGroup string
	}{
		schnutiboxUser,
	}); err != nil {
		return fmt.Errorf("could not write templated udev rules: %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
}

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

	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 fmt.Errorf("could not remove: %w", 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() 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.
	// nolint:gosec,gomnd
	if err := ioutil.WriteFile("/boot/cmdline.txt", []byte(newLine), 0o644); err != nil {
		return fmt.Errorf("could not write cmdline.txt: %w", err)
	}

	return nil
}

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

	if err := symlinks(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(); err != nil {
		return fmt.Errorf("could not modify cmdline.txt: %w", err)
	}

	return nil
}

// mopidy setups mopidy.
//nolint:funlen,cyclop
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",
		"libgstreamer-plugins-bad1.0",
		"mopidy",
		"mopidy-alsamixer",
		"mopidy-mpd",
		"mopidy-spotify",
		"python3-pip",
	)
	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)
	}

	// Extensions.
	cmd = exec.Command(
		"pip3", "install", "--upgrade",
		"Mopidy-YouTube",
		"requests>=2.22",
	)
	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 extensions: %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
	}

	tmpl, err := assets.Templates.ReadFile("templates/mopidy.conf.tmpl")
	if err != nil {
		return fmt.Errorf("could not get mopidy.conf: %w", err)
	}

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

	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.
	upmpdcliConf, err := assets.Files.ReadFile("files/upmpdcli.conf")
	if err != nil {
		return fmt.Errorf("could not get upmpdcli.conf: %w", err)
	}

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

	return nil
}

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

	// Download deb.
	cmd := exec.Command(
		"wget",
		"https://github.com/badaix/snapcast/releases/download/v0.24.0/snapclient_0.24.0-1_without-pulse_armhf.deb",
		"-O", "/tmp/snapclient.deb",
	)
	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 snapclient deb: %w", err)
	}

	// Install deb
	cmd = exec.Command(
		"/bin/sh", "-c",
		"dpkg -i /tmp/snapclient.deb; apt --fix-broken install -y; rm /tmp/snapclient.deb",
	)
	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 snapclient deb: %w", err)
	}

	// Download deb.
	cmd = exec.Command(
		"wget",
		"https://github.com/badaix/snapcast/releases/download/v0.24.0/snapserver_0.24.0-1_armhf.deb",
		"-O", "/tmp/snapserver.deb",
	)
	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 snapserver deb: %w", err)
	}

	// Install deb
	cmd = exec.Command(
		"/bin/sh", "-c",
		"dpkg -i /tmp/snapserver.deb; apt --fix-broken install -y; rm /tmp/snapserver.deb",
	)
	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 snapserver deb: %w", err)
	}

	return nil
}

func schnutiboxConfig() error {
	logger := log.With().Str("stage", "schnutiboxConfig").Logger()
	logger.Info().Msg("writing schnutibox config")

	// Parse template.
	tmpl, err := assets.Templates.ReadFile("templates/schnutibox.yml.tmpl")
	if err != nil {
		return fmt.Errorf("could not get template: %w", err)
	}

	t := template.Must(template.New("config").Parse(string(tmpl)))

	// Open file.
	f, err := os.Create("/etc/schnutibox/schnutibox.yml")
	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, Cfg); err != nil {
		return fmt.Errorf("could not write templated udev rules: %w", err)
	}

	return nil
}

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

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

	// Create schnutibox config.
	if err := schnutiboxConfig(); err != nil {
		log.Fatal().Err(err).Msg("could not create schnutibox config.")
	}

	// Install udev file.
	if err := udevRules(); err != nil {
		log.Fatal().Err(err).Msg("could not install udev rules")
	}

	// 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")
	}

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

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