package main

import (
	"crypto/md5"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"time"

	// Vendors
	"iscsi"
	"util"
	"github.com/Sirupsen/logrus"
	"github.com/docker/go-plugins-helpers/volume"
)

var iSCSI iscsi.ISCSIPlugin

type iscsiVolume struct {
	Host string
	TargetIQN string

	Options []string

	Mountpoint  string
	connections int
}

type iscsiDriver struct {
	sync.RWMutex

	root      string
	statePath string
	volumes   map[string]*iscsiVolume
}

// Constructor
func newiscsiDriver(root string) (*iscsiDriver, error) {
	logrus.WithField("method", "new driver").Debug(root)

	fi, err := os.Lstat(root)
	if os.IsNotExist(err) {
		if err := os.MkdirAll(root, 0755); err != nil {
			return nil, err
		}
	} else if err != nil {
		return nil, err
	}

	if fi != nil && !fi.IsDir() {
		return nil, util.LogError("%v already exist and it's not a directory", root)
	}

	stateFolder := filepath.Join(root, "plugins", "docker-iscsi")
	fi, err = os.Lstat(stateFolder)
	if os.IsNotExist(err) {
		if err := os.MkdirAll(stateFolder, 0755); err != nil {
			return nil, err
		}
	} else if err != nil {
		return nil, err
	}

	if fi != nil && !fi.IsDir() {
		return nil, util.LogError("%v already exist and it's not a directory", stateFolder)
	}

	d := &iscsiDriver{
		root:      filepath.Join(root, "volumes"),
		statePath: filepath.Join(stateFolder, "iscsi-state.json"),
		volumes:   map[string]*iscsiVolume{},
	}

	data, err := ioutil.ReadFile(d.statePath)
	if err != nil {
		if os.IsNotExist(err) {
			logrus.WithField("statePath", d.statePath).Debug("no state found")
		} else {
			return nil, err
		}
	} else {
		if err := json.Unmarshal(data, &d.volumes); err != nil {
			return nil, err
		}
	}

	return d, nil
}

// Docker API :- Create
func (d *iscsiDriver) Create(r *volume.CreateRequest) error {
	logrus.WithField("method", "create").Debugf("%#v", r)

	d.Lock()
	defer d.Unlock()

	v := &iscsiVolume{}

	for key, val := range r.Options {
		switch key {
		case "targetiqn":
			v.TargetIQN = val
		case "host":
			v.Host = val
		default:
			if val != "" {
				v.Options = append(v.Options, key+"="+val)
			} else {
				v.Options = append(v.Options, key)
			}
		}
	}

	if v.Host == "" {
		return util.LogError("'host' option required")
	}
	if v.TargetIQN == "" {
		return util.LogError("'targetiqn' option required")
	}

	v.Mountpoint = filepath.Join(d.root, fmt.Sprintf("%x", md5.Sum([]byte(v.TargetIQN))))

	d.volumes[r.Name] = v
	d.saveState()

	return nil
}

// Docker API :- Remove
func (d *iscsiDriver) Remove(r *volume.RemoveRequest) error {
	logrus.WithField("method", "remove").Debugf("%#v", r)

	d.Lock()
	defer d.Unlock()

	v, ok := d.volumes[r.Name]
	if !ok {
		return util.LogError("volume %s not found", r.Name)
	}

	if v.connections != 0 {
		return util.LogError("volume %s is currently used by a container", r.Name)
	}
	if err := os.RemoveAll(v.Mountpoint); err != nil {
		return util.LogError(err.Error())
	}

	delete(d.volumes, r.Name)
	d.saveState()

	return nil
}

// Docker API :- Mount
func (d *iscsiDriver) Mount(r *volume.MountRequest) (*volume.MountResponse, error) {
	logrus.WithField("method", "mount").Debugf("%#v", r)

	d.Lock()
	defer d.Unlock()

	v, ok := d.volumes[r.Name]
	if !ok {
		return &volume.MountResponse{}, util.LogError("volume %s not found", r.Name)
	}

	if v.connections == 0 {
		fi, err := os.Lstat(v.Mountpoint)
		if os.IsNotExist(err) {
			if err := os.MkdirAll(v.Mountpoint, 0755); err != nil {
				return &volume.MountResponse{}, util.LogError(err.Error())
			}
		} else if err != nil {
			return &volume.MountResponse{}, util.LogError(err.Error())
		}

		if fi != nil && !fi.IsDir() {
			return &volume.MountResponse{}, util.LogError("%v already exist and it's not a directory", v.Mountpoint)
		}

		if err := d.mountVolume(v); err != nil {
			return &volume.MountResponse{}, util.LogError(err.Error())
		}
	}

	v.connections++

	return &volume.MountResponse{Mountpoint: v.Mountpoint}, nil
}

// Docker API :- Path
func (d *iscsiDriver) Path(r *volume.PathRequest) (*volume.PathResponse, error) {
	logrus.WithField("method", "path").Debugf("%#v", r)

	d.RLock()
	defer d.RUnlock()

	v, ok := d.volumes[r.Name]
	if !ok {
		return &volume.PathResponse{}, util.LogError("volume %s not found", r.Name)
	}

	return &volume.PathResponse{Mountpoint: v.Mountpoint}, nil
}

// Docker API :- Unmount
func (d *iscsiDriver) Unmount(r *volume.UnmountRequest) error {
	logrus.WithField("method", "unmount").Debugf("%#v", r)

	d.Lock()
	defer d.Unlock()

	v, ok := d.volumes[r.Name]
	if !ok {
		return util.LogError("volume %s not found", r.Name)
	}

	v.connections--

	if v.connections <= 0 {
		if err := d.unmountVolume(v); err != nil {
			return util.LogError(err.Error())
		}
		v.connections = 0
	}

	return nil
}

// Docker API :- Get
func (d *iscsiDriver) Get(r *volume.GetRequest) (*volume.GetResponse, error) {
	logrus.WithField("method", "get").Debugf("%#v", r)

	d.Lock()
	defer d.Unlock()

	v, ok := d.volumes[r.Name]
	if !ok {
		return &volume.GetResponse{}, util.LogError("volume %s not found", r.Name)
	}

	return &volume.GetResponse{Volume: &volume.Volume{Name: r.Name, Mountpoint: v.Mountpoint}}, nil
}

// Docker API :- List
func (d *iscsiDriver) List() (*volume.ListResponse, error) {
	logrus.WithField("method", "list").Debugf("")

	d.Lock()
	defer d.Unlock()

	var vols []*volume.Volume
	for name, v := range d.volumes {
		vols = append(vols, &volume.Volume{Name: name, Mountpoint: v.Mountpoint})
	}
	return &volume.ListResponse{Volumes: vols}, nil
}

// Docker API :- Capabilities
func (d *iscsiDriver) Capabilities() *volume.CapabilitiesResponse {
	logrus.WithField("method", "capabilities").Debugf("")

	return &volume.CapabilitiesResponse{Capabilities: volume.Capability{Scope: "local"}}
}

// Local Functions
func (d *iscsiDriver) mountVolume(v *iscsiVolume) error {
	// Discover iSCSI LUNs
	err := iSCSI.DiscoverLUNs(v.Host)
	if err != nil {
		return err
	}

	if !iSCSI.TargetLoggedIn(v.TargetIQN) {
		// Login to iSCSI Target
		err = iSCSI.LoginTarget(v.TargetIQN, v.Host)
		if err != nil {
			return err
		}

		// Wait for Physical Volume to appear
		time.Sleep(500 * time.Millisecond)
	}

	out := ""
	errMsg := ""

	// TODO: Move this feature to iscsi driver

	// Get target device
	cmd := "iscsiadm -m session | grep \"" + v.TargetIQN + "\" | head -n 1 | sed -e 's/.*\\[\\(.*\\)\\].*/\\1/g'"
	out, errMsg = util.ExecuteCommandString(cmd)

	if len(out) > 0 {
		logrus.Debug(out)
	}
	if len(errMsg) > 0 {
		err := fmt.Errorf("Unable to find device: %s", errMsg)
		return err
	}

	out = strings.TrimSuffix(out, "\n")

	cmd = "iscsiadm -m session -r " + out + " -P 3 | grep \"Attached scsi disk\" | sed -e 's/.*Attached scsi disk \\([^\\t]*\\).*/\\/dev\\/\\1/g'"
	out, errMsg = util.ExecuteCommandString(cmd)

	if len(out) > 0 {
		logrus.Debug(out)
	}
	if len(errMsg) > 0 {
		err := fmt.Errorf("Unable to locate device: %s", errMsg)
		return err
	}

	out = strings.TrimSuffix(out, "\n")

	// Mount
	cmd = "mount " + out + " " + v.Mountpoint
	out, errMsg = util.ExecuteCommandString(cmd)

	if len(out) > 0 {
		logrus.Debug(out)
	}
	if len(errMsg) > 0 {
		err := fmt.Errorf("Unable to Mount Volume: %s", errMsg)
		return err
	}

	return nil
}

func (d *iscsiDriver) unmountVolume(v *iscsiVolume) error {
	// Unmount
	cmd := "umount " + v.Mountpoint
	out, errMsg := util.ExecuteCommandString(cmd)

	if len(out) > 0 {
		logrus.Debug(out)
	}
	if len(errMsg) > 0 {
		err := fmt.Errorf("Unable to Unmount Volume: %s", errMsg)
		logrus.Info(err)
	}

	if iSCSI.TargetLoggedIn(v.TargetIQN) {
		// Logout from iSCSI Target
		err := iSCSI.LogoutTarget(v.TargetIQN, v.Host)
		if err != nil {
			return err
		}
	}

	return nil
}

func (d *iscsiDriver) saveState() {
	data, err := json.Marshal(d.volumes)

	if err != nil {
		logrus.WithField("statePath", d.statePath).Error(err)
		return
	}

	if err := ioutil.WriteFile(d.statePath, data, 0644); err != nil {
		logrus.WithField("savestate", d.statePath).Error(err)
	}
}

