package router

import (
	"bytes"
	"errors"
	"fmt"
	"os"
	"os/exec"
	"os/signal"
	"path/filepath"
	"strings"
	"syscall"
	"text/template"

	"github.com/kardianos/service"

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

const (
	merlinJFFSScriptPath             = "/jffs/scripts/services-start"
	merlinJFFSServiceEventScriptPath = "/jffs/scripts/service-event"
)

type merlinSvc struct {
	i        service.Interface
	platform string
	*service.Config
}

func newMerlinService(i service.Interface, platform string, c *service.Config) (service.Service, error) {
	s := &merlinSvc{
		i:        i,
		platform: platform,
		Config:   c,
	}
	return s, nil
}

func (s *merlinSvc) String() string {
	if len(s.DisplayName) > 0 {
		return s.DisplayName
	}
	return s.Name
}

func (s *merlinSvc) Platform() string {
	return s.platform
}

func (s *merlinSvc) configPath() string {
	bin := s.Config.Executable
	if bin == "" {
		path, err := os.Executable()
		if err != nil {
			return ""
		}
		bin = path
	}
	return bin + ".startup"
}

func (s *merlinSvc) template() *template.Template {
	return template.Must(template.New("").Parse(merlinSvcScript))
}

func (s *merlinSvc) Install() error {
	exePath, err := os.Executable()
	if err != nil {
		return err
	}

	if !strings.HasPrefix(exePath, "/jffs/") {
		return errors.New("could not install service outside /jffs")
	}
	if _, err := nvram.Run("set", "jffs2_scripts=1"); err != nil {
		return err
	}
	if _, err := nvram.Run("commit"); err != nil {
		return err
	}

	confPath := s.configPath()
	if _, err := os.Stat(confPath); err == nil {
		return fmt.Errorf("already installed: %s", confPath)
	}

	var to = &struct {
		*service.Config
		Path string
	}{
		s.Config,
		exePath,
	}

	f, err := os.Create(confPath)
	if err != nil {
		return fmt.Errorf("os.Create: %w", err)
	}
	defer f.Close()

	if err := s.template().Execute(f, to); err != nil {
		return fmt.Errorf("s.template.Execute: %w", err)
	}

	if err = os.Chmod(confPath, 0755); err != nil {
		return fmt.Errorf("os.Chmod: startup script: %w", err)
	}

	if err := os.MkdirAll(filepath.Dir(merlinJFFSScriptPath), 0755); err != nil {
		return fmt.Errorf("os.MkdirAll: %w", err)
	}

	tmpScript, err := os.CreateTemp("", "ctrld_install")
	if err != nil {
		return fmt.Errorf("os.CreateTemp: %w", err)
	}
	defer os.Remove(tmpScript.Name())
	defer tmpScript.Close()

	if _, err := tmpScript.WriteString(merlinAddLineToScript); err != nil {
		return fmt.Errorf("tmpScript.WriteString: %w", err)
	}
	if err := tmpScript.Close(); err != nil {
		return fmt.Errorf("tmpScript.Close: %w", err)
	}
	addLineToScript := func(line, script string) error {
		if _, err := os.Stat(script); os.IsNotExist(err) {
			if err := os.WriteFile(script, []byte("#!/bin/sh\n"), 0755); err != nil {
				return err
			}
		}
		if err := os.Chmod(script, 0755); err != nil {
			return fmt.Errorf("os.Chmod: jffs script: %w", err)
		}

		if err := exec.Command("sh", tmpScript.Name(), line, script).Run(); err != nil {
			return fmt.Errorf("exec.Command: add startup script: %w", err)
		}
		return nil
	}

	for script, line := range map[string]string{
		merlinJFFSScriptPath:             s.configPath() + " start",
		merlinJFFSServiceEventScriptPath: s.configPath() + ` service_event "$1" "$2"`,
	} {
		if err := addLineToScript(line, script); err != nil {
			return err
		}
	}

	return nil
}

func (s *merlinSvc) Uninstall() error {
	if err := os.Remove(s.configPath()); err != nil {
		return fmt.Errorf("os.Remove: %w", err)
	}
	tmpScript, err := os.CreateTemp("", "ctrld_uninstall")
	if err != nil {
		return fmt.Errorf("os.CreateTemp: %w", err)
	}
	defer os.Remove(tmpScript.Name())
	defer tmpScript.Close()

	if _, err := tmpScript.WriteString(merlinRemoveLineFromScript); err != nil {
		return fmt.Errorf("tmpScript.WriteString: %w", err)
	}
	if err := tmpScript.Close(); err != nil {
		return fmt.Errorf("tmpScript.Close: %w", err)
	}
	removeLineFromScript := func(line, script string) error {
		if _, err := os.Stat(script); os.IsNotExist(err) {
			if err := os.WriteFile(script, []byte("#!/bin/sh\n"), 0755); err != nil {
				return err
			}
		}
		if err := os.Chmod(script, 0755); err != nil {
			return fmt.Errorf("os.Chmod: jffs script: %w", err)
		}

		if err := exec.Command("sh", tmpScript.Name(), line, script).Run(); err != nil {
			return fmt.Errorf("exec.Command: add startup script: %w", err)
		}
		return nil
	}

	for script, line := range map[string]string{
		merlinJFFSScriptPath:             s.configPath() + " start",
		merlinJFFSServiceEventScriptPath: s.configPath() + ` service_event "$1" "$2"`,
	} {
		if err := removeLineFromScript(line, script); err != nil {
			return err
		}
	}

	return nil
}

func (s *merlinSvc) Logger(errs chan<- error) (service.Logger, error) {
	if service.Interactive() {
		return service.ConsoleLogger, nil
	}
	return s.SystemLogger(errs)
}

func (s *merlinSvc) SystemLogger(errs chan<- error) (service.Logger, error) {
	return newSysLogger(s.Name, errs)
}

func (s *merlinSvc) Run() (err error) {
	err = s.i.Start(s)
	if err != nil {
		return err
	}

	if interactice, _ := isInteractive(); !interactice {
		signal.Ignore(syscall.SIGHUP)
	}

	var sigChan = make(chan os.Signal, 1)
	signal.Notify(sigChan, syscall.SIGTERM, os.Interrupt)
	<-sigChan

	return s.i.Stop(s)
}

func (s *merlinSvc) Status() (service.Status, error) {
	if _, err := os.Stat(s.configPath()); os.IsNotExist(err) {
		return service.StatusUnknown, service.ErrNotInstalled
	}
	out, err := exec.Command(s.configPath(), "status").CombinedOutput()
	if err != nil {
		return service.StatusUnknown, err
	}
	switch string(bytes.TrimSpace(out)) {
	case "running":
		return service.StatusRunning, nil
	default:
		return service.StatusStopped, nil
	}
}

func (s *merlinSvc) Start() error {
	return exec.Command(s.configPath(), "start").Run()
}

func (s *merlinSvc) Stop() error {
	return exec.Command(s.configPath(), "stop").Run()
}

func (s *merlinSvc) Restart() error {
	err := s.Stop()
	if err != nil {
		return err
	}
	return s.Start()
}

const merlinSvcScript = `#!/bin/sh

name="{{.Name}}"
cmd="{{.Path}}{{range .Arguments}} {{.}}{{end}}"
pid_file="/tmp/$name.pid"

get_pid() {
  cat "$pid_file"
}

is_running() {
  [ -f "$pid_file" ] && ps | grep -q "^ *$(get_pid) "
}

case "$1" in
  start)
    if is_running; then
      logger -c "Already started"
    else
      logger -c "Starting $name"
      if [ -f /rom/ca-bundle.crt ]; then
        # For John’s fork
        export SSL_CERT_FILE=/rom/ca-bundle.crt
      fi
      $cmd &
      echo $! > "$pid_file"
      chmod 600 "$pid_file"
      if ! is_running; then
       logger -c "Failed to start $name"
       exit 1
      fi
    fi
  ;;
  stop)
    if is_running; then
      logger -c "Stopping $name..."
      kill "$(get_pid)"
      for _ in 1 2 3 4 5; do
        if ! is_running; then
          logger -c "stopped"
          if [ -f "$pid_file" ]; then
            rm "$pid_file"
          fi
          exit 0
        fi
        printf "."
        sleep 2
      done
      logger -c "failed to stop $name"
      exit 1
    fi
    exit 0
  ;;
  restart)
    $0 stop
    $0 start
  ;;
  status)
    if is_running; then
      echo "running"
    else
      echo "stopped"
      exit 1
    fi
  ;;
  service_event)
    event=$2
    svc=$3
    dnsmasq_pid_file=$(sed -n '/pid-file=/s///p' /etc/dnsmasq.conf)

    if [ "$event" = "restart" ] && [ "$svc" = "diskmon" ]; then
      kill "$(cat "$dnsmasq_pid_file")" >/dev/null 2>&1
    fi
  ;;
  *)
    echo "Usage: $0 {start|stop|restart|status}"
    exit 1
  ;;
esac
exit 0
`

const merlinAddLineToScript = `#!/bin/sh

line=$1
file=$2

. /usr/sbin/helper.sh

pc_append "$line" "$file" 
`

const merlinRemoveLineFromScript = `#!/bin/sh

line=$1
file=$2

. /usr/sbin/helper.sh

pc_delete "$line" "$file" 
`
