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

	"github.com/asteris-llc/converge/resource/wait"
	"github.com/pkg/errors"
)

// LVM is a public interface to LVM guts for converge highlevel modules
type LVM interface {
	// Check for LVM tools installed and useable
	Check() error

	// Check for mkfs.* tools installed and useable
	CheckFilesystemTools(fstype string) error

	QueryLogicalVolumes(vg string) (map[string]*LogicalVolume, error)
	QueryPhysicalVolumes() (map[string]*PhysicalVolume, error)
	QueryVolumeGroups() (map[string]*VolumeGroup, error)
	CreateVolumeGroup(vg string, devs []string) error
	ExtendVolumeGroup(vg string, dev string) error
	ReduceVolumeGroup(vg string, dev string) error
	CreatePhysicalVolume(dev string) error
	RemovePhysicalVolume(dev string, force bool) error
	CreateLogicalVolume(group string, volume string, size *LvmSize) error
	Mkfs(dev string, fstype string) error
	Mountpoint(path string) (bool, error)
	Blkid(dev string) (string, error)
	WaitForDevice(path string) error

	// systemd units
	CheckUnit(filename string, content string) (bool, error)
	UpdateUnit(filename string, content string) error
	StartUnit(filename string) error
}

type realLVM struct {
	backend Exec
}

// MakeLvmBackend creates default LVM backend
func MakeLvmBackend() LVM {
	backend := MakeOsExec()
	return &realLVM{backend: backend}
}

// MakeRealLVM is actually kludge for DI (intended for creating test-backed RealLVM, and unpublish type inself)
func MakeRealLVM(backend Exec) LVM {
	return &realLVM{backend: backend}
}

func (lvm *realLVM) CreateVolumeGroup(vg string, devs []string) error {
	args := []string{vg}
	var canonicalDevs []string
	for _, dev := range devs {
		canonicalDev, err := lvm.backend.EvalSymlinks(dev)
		if err != nil {
			return errors.Wrap(err, "resolving symlink for "+dev)
		}
		canonicalDevs = append(canonicalDevs, canonicalDev)
	}
	args = append(args, canonicalDevs...)
	return lvm.backend.Run("vgcreate", args)
}

func (lvm *realLVM) ExtendVolumeGroup(vg string, dev string) error {
	canonicalDev, err := lvm.backend.EvalSymlinks(dev)
	if err != nil {
		return err
	}
	return lvm.backend.Run("vgextend", []string{vg, canonicalDev})
}

func (lvm *realLVM) ReduceVolumeGroup(vg string, dev string) error {
	canonicalDev, err := lvm.backend.EvalSymlinks(dev)
	if err != nil {
		return err
	}
	return lvm.backend.Run("vgreduce", []string{vg, canonicalDev})
}

func (lvm *realLVM) CreatePhysicalVolume(dev string) error {
	canonicalDev, err := lvm.backend.EvalSymlinks(dev)
	if err != nil {
		return err
	}
	return lvm.backend.Run("pvcreate", []string{canonicalDev})
}

func (lvm *realLVM) RemovePhysicalVolume(dev string, force bool) error {
	canonicalDev, err := lvm.backend.EvalSymlinks(dev)
	if err != nil {
		return err
	}
	args := []string{}
	if force {
		args = append(args, "--force", "--force", "--yes")
	}
	args = append(args, canonicalDev)
	return lvm.backend.Run("pvremove", args)
}

func (lvm *realLVM) CreateLogicalVolume(group string, volume string, size *LvmSize) error {
	sizeStr := size.String()
	option := size.Option()
	return lvm.backend.Run("lvcreate", []string{"-n", volume, option, sizeStr, group})
}

func (lvm *realLVM) Mkfs(dev string, fstype string) error {
	canonicalDev, err := lvm.backend.EvalSymlinks(dev)
	if err != nil {
		return err
	}
	return lvm.backend.Run("mkfs", []string{"-t", fstype, canonicalDev})
}

func (lvm *realLVM) Mountpoint(path string) (bool, error) {
	rc, err := lvm.backend.RunWithExitCode("mountpoint", []string{"-q", path})
	if err != nil {
		return false, err
	}
	if rc == 0 {
		return true, nil
	}
	return false, nil
}

func (lvm *realLVM) Check() error {
	if uid := lvm.backend.Getuid(); uid != 0 {
		return fmt.Errorf("lvm require root permissions (uid == 0), but converge run from user id (uid == %d)", uid)
	}
	// NB: extend list to all used tools or wrap all calls via `lvm $subcommand` and check for lvm only
	//     second way need careful check, if `lvm $subcommand` and just `$subcommand`  accepot exact same parameters
	// Related issue: https://github.com/asteris-llc/converge/issues/457
	for _, tool := range []string{"lvs", "vgs", "pvs", "lvcreate", "lvreduce", "lvremove", "vgcreate", "vgreduce", "pvcreate"} {
		if err := lvm.backend.Lookup(tool); err != nil {
			return errors.Wrapf(err, "lvm: can't find required tool %s in $PATH", tool)
		}
	}
	return nil
}

func (lvm *realLVM) CheckFilesystemTools(fstype string) error {
	// Root check just copied from .Check() because lvm.fs can be used w/o lvm utils,  but require root and mkfs.*
	if uid := lvm.backend.Getuid(); uid != 0 {
		return fmt.Errorf("lvm require root permissions (uid == 0), but converge run from user id (uid == %d)", uid)
	}

	tool := fmt.Sprintf("mkfs.%s", fstype)
	if err := lvm.backend.Lookup(tool); err != nil {
		return errors.Wrapf(err, "lvm: can't find required tool %s in $PATH", tool)
	}
	return nil
}

func (lvm *realLVM) WaitForDevice(path string) error {
	retrier := wait.PrepareRetrier(nil, nil, nil)
	ok, err := retrier.RetryUntil(func() (bool, error) {
		return lvm.backend.Exists(path)
	})
	if err != nil {
		return err
	}
	if !ok {
		return fmt.Errorf("device path %s not appeared after %s seconds", path, retrier.Duration.String())
	}
	return nil
}
