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

import (
	"bytes"
	"fmt"
	"strings"
	"text/template"

	log "github.com/Sirupsen/logrus"
	"github.com/asteris-llc/converge/resource"
	"github.com/asteris-llc/converge/resource/lvm/lowlevel"
	"github.com/pkg/errors"
	"golang.org/x/net/context"
)

type resourceFS struct {
	mount           *Mount
	lvm             lowlevel.LVM
	unitFileName    string
	unitFileContent string
	unitNeedUpdate  bool
	mountNeedUpdate bool
	needMkfs        bool
}

// Mount is a structure for holding values to be rendered as .mount unit for systemd
type Mount struct {
	What       string
	Where      string
	Type       string
	Before     string
	WantedBy   string
	RequiredBy string
}

// NB: RequiredBy statement should issued only when non-empty
// Related issue: https://github.com/asteris-llc/converge/issues/452
const unitTemplate = `[Unit]
Before=local-fs.target {{.Before}}

[Mount]
What={{.What}}
Where={{.Where}}
Type={{.Type}}

[Install]
WantedBy=local-fs.target {{.WantedBy}}
RequiredBy={{.RequiredBy}}`

func (r *resourceFS) Check(context.Context, resource.Renderer) (resource.TaskStatus, error) {
	status := &resource.Status{}

	if err := r.lvm.CheckFilesystemTools(r.mount.Type); err != nil {
		return nil, errors.Wrapf(err, "filesystem prerequisites for %s", r.mount.Type)
	}

	if err := r.checkBlkid(status); err != nil {
		return nil, err
	}

	if err := r.checkUnit(status); err != nil {
		return nil, err
	}

	if err := r.checkMountpoint(status); err != nil {
		return nil, err
	}

	status.RaiseLevelForDiffs()

	return status, nil
}

func (r *resourceFS) Apply(context.Context) (resource.TaskStatus, error) {
	if r.needMkfs {
		if err := r.lvm.Mkfs(r.mount.What, r.mount.Type); err != nil {
			return nil, errors.Wrapf(err, "mkfs")
		}
	}

	if r.unitNeedUpdate {
		if err := r.lvm.UpdateUnit(r.unitFileName, r.unitFileContent); err != nil {
			return nil, errors.Wrapf(err, "updating unit file %s", r.unitFileName)
		}
	}

	if r.mountNeedUpdate {
		service := r.unitServiceName()
		if err := r.lvm.StartUnit(service); err != nil {
			return nil, errors.Wrapf(err, "starting service %s", service)
		}
	}

	r.unitNeedUpdate = false
	r.mountNeedUpdate = false
	return &resource.Status{}, nil
}

// NewResourceFS create new resource.Task node for create/mount FileSystem.
func NewResourceFS(lvm lowlevel.LVM, m *Mount) (resource.Task, error) {
	var err error
	r := &resourceFS{
		lvm:   lvm,
		mount: m,
	}
	r.unitFileName = r.unitName()
	r.unitFileContent, err = r.renderUnitFile()
	if err != nil {
		return nil, err
	}
	return r, nil
}

func (r *resourceFS) checkBlkid(status *resource.Status) error {
	fs, err := r.lvm.Blkid(r.mount.What)
	if err != nil {
		return errors.Wrapf(err, "retrieving current FS type of %s", r.mount.What)
	}
	log.Debugf("blkid detect following fstype: %s, planned fstype: %s", fs, r.mount.Type)
	if fs == r.mount.Type {
		r.needMkfs = false
	} else if fs == "" {
		r.needMkfs = true
		status.AddDifference("format", "<unformatted>", r.mount.Type, "")
	} else {
		return fmt.Errorf("%s already contain other filesystem with different type %s", r.mount.What, fs)
	}
	return nil
}

// NB: Here we need to ensure, that r. mount.Where is exists, and have proper permissions
// Related issue: https://github.com/asteris-llc/converge/issues/449
//
// NB: We need to ensure, that no other filesystems mounted to Where
// (now we check only if it mountpoint or not)
// Related issue: https://github.com/asteris-llc/converge/issues/450
func (r *resourceFS) checkMountpoint(status *resource.Status) error {
	ok, err := r.lvm.Mountpoint(r.mount.Where)
	if err != nil {
		return errors.Wrapf(err, "querying mountpoint %s", r.mount.Where)
	}
	r.mountNeedUpdate = r.unitNeedUpdate || !ok

	if r.mountNeedUpdate {
		status.AddDifference(r.mount.Where, "<none>", fmt.Sprintf("mount %s", r.mount.Where), "")
	}
	return nil
}

func (r *resourceFS) checkUnit(status *resource.Status) error {
	ok, err := r.lvm.CheckUnit(r.unitFileName, r.unitFileContent)
	if err != nil {
		return err
	}
	if ok {
		r.unitNeedUpdate = true
		status.AddDifference(r.unitFileName, "<none>", r.unitFileContent, "")
	}
	return nil
}

func (r *resourceFS) escapedMountpoint() string {
	// NB: proper systemd' escaping of r.mountpoint should be implemented here
	// Related issue: https://github.com/asteris-llc/converge/issues/451
	return strings.Replace(strings.Trim(r.mount.Where, "/"), "/", "-", -1)
}

func (r *resourceFS) unitServiceName() string {
	return fmt.Sprintf("%s.mount", r.escapedMountpoint())
}

func (r *resourceFS) unitName() string {
	return fmt.Sprintf("/etc/systemd/system/%s", r.unitServiceName())
}

func (r *resourceFS) renderUnitFile() (string, error) {
	var b bytes.Buffer
	tmpl, err := template.New("unit.mount").Parse(unitTemplate)
	if err != nil {
		return "", errors.Wrapf(err, "renderUnitFile(): template syntax")
	}
	err = tmpl.Execute(&b, r.mount)
	if err != nil {
		return "", errors.Wrapf(err, "renderUnitFile(): template executing")
	}
	return b.String(), nil
}
