// Copyright © 2016 Asteris, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package lowlevel

import (
	"fmt"
	"io/ioutil"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"syscall"

	log "github.com/Sirupsen/logrus"
	"github.com/pkg/errors"
)

// Exec is interface to `real system` also used for test injections
// NB: should be reviewed, and possible refactored as base for all
//     processes/filesystem operrations, to make the easy mockable
// NB: possoble split to ExecInterface, FilesystemInterface (possible also SystemInterface for Getuid)
// Related issue: https://github.com/asteris-llc/converge/issues/455
type Exec interface {
	Run(prog string, args []string) error
	RunWithExitCode(prog string, args []string) (int, error) // for mountpoint querying
	Read(prog string, args []string) (stdout string, err error)
	ReadWithExitCode(prog string, args []string) (stdout string, rc int, err error) // for blkid querying
	Lookup(prog string) error
	Getuid() int

	// unit read/write injection
	ReadFile(fn string) ([]byte, error)
	WriteFile(fn string, c []byte, p os.FileMode) error
	MkdirAll(path string, perm os.FileMode) error
	Exists(path string) (bool, error)

	// Local Filesystem Functions
	EvalSymlinks(string) (string, error)
}

type osExec struct {
}

// MakeOsExec create Exec backend
func MakeOsExec() Exec {
	return &osExec{}
}

func (*osExec) EvalSymlinks(path string) (string, error) {
	return filepath.EvalSymlinks(path)
}

func (*osExec) Run(prog string, args []string) error {
	log.WithField("module", "lvm").Infof("Executing %s: %v", prog, args)
	e := exec.Command(prog, args...).Run()
	if e == nil {
		log.WithField("module", "lvm").Debugf("%s: no error", prog)
	} else {
		log.WithField("module", "lvm").Debugf("%s: terminated with %s", prog, e.Error())
	}
	return e
}

func (e *osExec) RunWithExitCode(prog string, args []string) (int, error) {
	err := e.Run(prog, args)
	return exitStatus(err)
}

func (*osExec) ReadWithExitCode(prog string, args []string) (stdout string, rc int, err error) {
	rc = 0
	log.WithField("module", "lvm").Infof("Executing (read) %s: %v", prog, args)
	out, err := exec.Command(prog, args...).Output()
	strOut := strings.Trim(string(out), "\n ")
	if err != nil {
		rc, err = exitStatus(err)
		if err != nil {
			log.WithField("module", "lvm").Debugf("%s: terminated with %s", prog, err.Error())
			return "", 0, errors.Wrapf(err, "reading output of process %s: %s", prog, args)
		}
	}
	return strOut, rc, err
}

func (e *osExec) Read(prog string, args []string) (stdout string, err error) {
	out, rc, err := e.ReadWithExitCode(prog, args)
	if err != nil {
		return "", err
	}
	if rc != 0 {
		return "", fmt.Errorf("process %s: %s terminated with status code %d", prog, args, rc)
	}
	return out, nil
}

func exitStatus(err error) (int, error) {
	if exiterr, ok := err.(*exec.ExitError); ok {
		// The program has exited with an exit code != 0
		// This works on both Unix and Windows. Although package
		// syscall is generally platform dependent, WaitStatus is
		// defined for both Unix and Windows and in both cases has
		// an ExitStatus() method with the same signature.
		if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
			return status.ExitStatus(), nil
		}
	}
	return 0, err
}

func (*osExec) Lookup(prog string) error {
	_, err := exec.LookPath(prog)
	return err
}

func (*osExec) ReadFile(fn string) ([]byte, error) {
	log.WithField("module", "lvm").Debugf("Reading %s...", fn)
	return ioutil.ReadFile(fn)
}

func (*osExec) WriteFile(fn string, content []byte, perm os.FileMode) error {
	log.WithField("module", "lvm").Debugf("Writing %s...", fn)
	return ioutil.WriteFile(fn, content, perm)
}

func (*osExec) MkdirAll(path string, perm os.FileMode) error {
	log.WithField("module", "lvm").Debugf("Make path %s...", path)
	return os.MkdirAll(path, perm)
}

func (*osExec) Exists(path string) (bool, error) {
	if _, err := os.Stat(path); err != nil {
		if os.IsNotExist(err) {
			return false, nil
		}
		return false, err
	}
	return true, nil
}

func (*osExec) Getuid() int {
	return os.Getuid()
}
