package netgear

import (
	"bufio"
	"bytes"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"strings"

	"github.com/kardianos/service"

	"github.com/Control-D-Inc/ctrld"
	"github.com/Control-D-Inc/ctrld/internal/router/dnsmasq"
	"github.com/Control-D-Inc/ctrld/internal/router/nvram"
)

const (
	Name                                = "netgear_orbi_voxel"
	netgearOrbiVoxelDNSMasqConfigPath   = "/etc/dnsmasq.conf"
	netgearOrbiVoxelHomedir             = "/mnt/bitdefender"
	netgearOrbiVoxelStartupScript       = "/mnt/bitdefender/rc.user"
	netgearOrbiVoxelStartupScriptBackup = "/mnt/bitdefender/rc.user.bak"
	netgearOrbiVoxelStartupScriptMarker = "\n# GENERATED BY ctrld"
)

var nvramKvMap = map[string]string{
	"dns_hijack": "0", // Disable dns hijacking
}

type NetgearOrbiVoxel struct {
	cfg *ctrld.Config
}

// New returns a router.Router for configuring/setup/run ctrld on ddwrt routers.
func New(cfg *ctrld.Config) *NetgearOrbiVoxel {
	return &NetgearOrbiVoxel{cfg: cfg}
}

func (d *NetgearOrbiVoxel) ConfigureService(svc *service.Config) error {
	if err := d.checkInstalledDir(); err != nil {
		return err
	}
	svc.Option["SysvScript"] = openWrtScript
	return nil
}

func (d *NetgearOrbiVoxel) Install(_ *service.Config) error {
	// Ignoring error here at this moment is ok, since everything will be wiped out on reboot.
	_ = exec.Command("/etc/init.d/ctrld", "enable").Run()
	if err := d.checkInstalledDir(); err != nil {
		return err
	}
	if err := backupVoxelStartupScript(); err != nil {
		return fmt.Errorf("backup startup script: %w", err)
	}
	if err := writeVoxelStartupScript(); err != nil {
		return fmt.Errorf("writing startup script: %w", err)
	}
	return nil
}

func (d *NetgearOrbiVoxel) Uninstall(_ *service.Config) error {
	if err := os.Remove(netgearOrbiVoxelStartupScript); err != nil && !os.IsNotExist(err) {
		return err
	}
	err := os.Rename(netgearOrbiVoxelStartupScriptBackup, netgearOrbiVoxelStartupScript)
	if err != nil && !os.IsNotExist(err) {
		return err
	}
	return nil
}

func (d *NetgearOrbiVoxel) PreRun() error {
	return nil
}

func (d *NetgearOrbiVoxel) Setup() error {
	if d.cfg.FirstListener().IsDirectDnsListener() {
		return nil
	}
	// Already setup.
	if val, _ := nvram.Run("get", nvram.CtrldSetupKey); val == "1" {
		return nil
	}

	data, err := dnsmasq.ConfTmplWithCacheDisabled(dnsmasq.ConfigContentTmpl, d.cfg, false)
	if err != nil {
		return err
	}
	currentConfig, _ := os.ReadFile(netgearOrbiVoxelDNSMasqConfigPath)
	configContent := append(currentConfig, data...)
	if err := os.WriteFile(netgearOrbiVoxelDNSMasqConfigPath, configContent, 0600); err != nil {
		return err
	}
	// Restart dnsmasq service.
	if err := restartDNSMasq(); err != nil {
		return err
	}

	if err := nvram.SetKV(nvramKvMap, nvram.CtrldSetupKey); err != nil {
		return err
	}

	return nil
}

func (d *NetgearOrbiVoxel) Cleanup() error {
	if d.cfg.FirstListener().IsDirectDnsListener() {
		return nil
	}
	if val, _ := nvram.Run("get", nvram.CtrldSetupKey); val != "1" {
		return nil // was restored, nothing to do.
	}

	// Restore old configs.
	if err := nvram.Restore(nvramKvMap, nvram.CtrldSetupKey); err != nil {
		return err
	}

	// Restore dnsmasq config.
	if err := restoreDnsmasqConf(); err != nil {
		return err
	}

	// Restart dnsmasq service.
	if err := restartDNSMasq(); err != nil {
		return err
	}
	return nil
}

// checkInstalledDir checks that ctrld binary was installed in the correct directory.
func (d *NetgearOrbiVoxel) checkInstalledDir() error {
	exePath, err := os.Executable()
	if err != nil {
		return fmt.Errorf("checkHomeDir: failed to get binary path %w", err)
	}
	if !strings.HasSuffix(filepath.Dir(exePath), netgearOrbiVoxelHomedir) {
		return fmt.Errorf("checkHomeDir: could not install service outside %s", netgearOrbiVoxelHomedir)
	}
	return nil
}

// backupVoxelStartupScript creates a backup of original startup script if existed.
func backupVoxelStartupScript() error {
	// Do nothing if the startup script was modified by ctrld.
	script, _ := os.ReadFile(netgearOrbiVoxelStartupScript)
	if bytes.Contains(script, []byte(netgearOrbiVoxelStartupScriptMarker)) {
		return nil
	}
	err := os.Rename(netgearOrbiVoxelStartupScript, netgearOrbiVoxelStartupScriptBackup)
	if err != nil && !os.IsNotExist(err) {
		return fmt.Errorf("backupVoxelStartupScript: %w", err)
	}
	return nil
}

// writeVoxelStartupScript writes startup script to re-install ctrld upon reboot.
// See: https://github.com/SVoxel/ORBI-RBK50/pull/7
func writeVoxelStartupScript() error {
	exe, err := os.Executable()
	if err != nil {
		return fmt.Errorf("configure service: failed to get binary path %w", err)
	}
	// This is called when "ctrld start ..." runs, so recording
	// the same command line arguments to use in startup script.
	argStr := strings.Join(os.Args[1:], " ")
	script, _ := os.ReadFile(netgearOrbiVoxelStartupScriptBackup)
	script = append(script, fmt.Sprintf("%s\n%q %s\n", netgearOrbiVoxelStartupScriptMarker, exe, argStr)...)
	f, err := os.Create(netgearOrbiVoxelStartupScript)
	if err != nil {
		return fmt.Errorf("failed to create startup script: %w", err)
	}
	defer f.Close()

	if _, err := f.Write(script); err != nil {
		return fmt.Errorf("failed to write startup script: %w", err)
	}
	if err := f.Close(); err != nil {
		return fmt.Errorf("failed to save startup script: %w", err)
	}
	return nil
}

// restoreDnsmasqConf restores original dnsmasq configuration.
func restoreDnsmasqConf() error {
	f, err := os.Open(netgearOrbiVoxelDNSMasqConfigPath)
	if err != nil {
		return err
	}
	defer f.Close()

	var bs []byte
	buf := bytes.NewBuffer(bs)

	removed := false
	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		line := scanner.Text()
		if line == dnsmasq.CtrldMarker {
			removed = true
		}
		if !removed {
			_, err := buf.WriteString(line + "\n")
			if err != nil {
				return err
			}
		}
	}
	return os.WriteFile(netgearOrbiVoxelDNSMasqConfigPath, buf.Bytes(), 0644)
}

func restartDNSMasq() error {
	if out, err := exec.Command("/etc/init.d/dnsmasq", "restart").CombinedOutput(); err != nil {
		return fmt.Errorf("restartDNSMasq: %s, %w", string(out), err)
	}
	return nil
}
