// Copyright (C) 2015 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.

package versioner

import (
	"context"
	"errors"
	"fmt"
	"os"
	"os/exec"
	"strings"
	"time"

	"github.com/syncthing/syncthing/lib/build"
	"github.com/syncthing/syncthing/lib/config"
	"github.com/syncthing/syncthing/lib/fs"

	"github.com/kballard/go-shellquote"
)

func init() {
	// Register the constructor for this type of versioner with the name "external"
	factories["external"] = newExternal
}

type external struct {
	command    string
	filesystem fs.Filesystem
}

func newExternal(cfg config.FolderConfiguration) Versioner {
	command := cfg.Versioning.Params["command"]

	if build.IsWindows {
		command = strings.ReplaceAll(command, `\`, `\\`)
	}

	s := external{
		command:    command,
		filesystem: cfg.Filesystem(nil),
	}

	l.Debugf("instantiated %#v", s)
	return s
}

// Archive moves the named file away to a version archive. If this function
// returns nil, the named file does not exist any more (has been archived).
func (v external) Archive(filePath string) error {
	info, err := v.filesystem.Lstat(filePath)
	if fs.IsNotExist(err) {
		l.Debugln("not archiving nonexistent file", filePath)
		return nil
	} else if err != nil {
		return err
	}
	if info.IsSymlink() {
		panic("bug: attempting to version a symlink")
	}

	l.Debugln("archiving", filePath)

	if v.command == "" {
		return errors.New("command is empty, please enter a valid command")
	}

	words, err := shellquote.Split(v.command)
	if err != nil {
		return fmt.Errorf("command is invalid: %w", err)
	}

	context := map[string]string{
		"%FOLDER_FILESYSTEM%": v.filesystem.Type().String(),
		"%FOLDER_PATH%":       v.filesystem.URI(),
		"%FILE_PATH%":         filePath,
	}

	for i, word := range words {
		for key, val := range context {
			word = strings.ReplaceAll(word, key, val)
		}

		words[i] = word
	}

	cmd := exec.Command(words[0], words[1:]...)
	env := os.Environ()
	// filter STGUIAUTH and STGUIAPIKEY from environment variables
	var filteredEnv []string

	for _, x := range env {
		if !strings.HasPrefix(x, "STGUIAUTH=") && !strings.HasPrefix(x, "STGUIAPIKEY=") {
			filteredEnv = append(filteredEnv, x)
		}
	}
	cmd.Env = filteredEnv
	combinedOutput, err := cmd.CombinedOutput()
	l.Debugln("external command output:", string(combinedOutput))
	if err != nil {
		if eerr, ok := err.(*exec.ExitError); ok && len(eerr.Stderr) > 0 {
			return fmt.Errorf("%v: %v", err, string(eerr.Stderr))
		}
		return err
	}

	// return error if the file was not removed
	if _, err = v.filesystem.Lstat(filePath); fs.IsNotExist(err) {
		return nil
	}
	return errors.New("file was not removed by external script")
}

func (external) GetVersions() (map[string][]FileVersion, error) {
	return nil, ErrRestorationNotSupported
}

func (external) Restore(_ string, _ time.Time) error {
	return ErrRestorationNotSupported
}

func (external) Clean(_ context.Context) error {
	return nil
}
