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

import (
	"fmt"

	"github.com/pkg/errors"

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

// Resource is the resource struct for systemd.unit.
//
// Many of the exported fields are derived from the systemd dbus api; see
// https://www.freedesktop.org/wiki/Software/systemd/dbus/ for a full
// description of the potential values for this fields and their meanings.
type Resource struct {
	// The name of the unit, including the unit type.
	Name string `export:"unit"`

	// The desired state of the unit as configured by the user. It will be one of
	// `running`, `stopped`, or `restarted` if it was configured by the user, and
	// an empty string otherwise.
	State string `export:"state"`

	// This field is set to true if the reload flag was configured by the user.
	Reload bool `export:"reload"`

	// The human-readable name of a unix signal that will be sent to the process.
	// If this is set the name will match the field set in SignalNumber.  See the
	// man pages for `signal(3)` on BSD/Darwin or `signal(7)` on GNU Linux for a
	// full explanation of these signals.
	SignalName string `export:"signal_name"`

	// The numeric identifier of a unix signal that will be sent to the process.
	// If this is set it will match the field set in SignalName.  See the man
	// pages for `signal(3)` on BSD/Darwin or `signal(7)` on GNU Linux for a full
	// explanation of these signals.
	SignalNumber uint `export:"signal_number"`

	// The full path to the unit file on disk. This field will be empty if the
	// unit was not started from a systemd unit file on disk.
	Path string `export:"path"`

	// Description of the services. This field will be empty unless a description
	// has been provided in the systemd unit file.
	Description string `export:"description"`

	// The active state of the unit. It will always be one of: `active`,
	// `reloading`, `inactive`, `failed`, `activating`, `deactivating`.
	ActiveState string `export:"activestate"`

	// The load state of the unit.
	LoadState string `export:"loadstate"`

	// The type of the unit as an enumerated value.  See TypeStr for a human
	// readable type.
	Type UnitType `export:"type"`

	// The type of the unit as a human readable string.  See the man page for
	// `systemd(1)` for a full description of the types and their meaning.
	TypeStr string `export:"typestr"`

	// The status represents the current status of the process.  It will be
	// initialized during planning and updated after apply to reflect any changes.
	Status string `export:"status"`

	// Properties are the global systemd unit properties and will be set for all
	// unit types. See the [systemd_Properties]({{< ref "properties.md" >}}) docs
	// for more information.
	Properties Properties `re-export-as:"global_properties"`

	// ServiceProperties contain properties specific to Service unit types. This
	// field is only exported when the unit type is `service`. See the
	// [systemd_ServiceTypeProperties]({{< ref "service_properties.md" >}}) docs
	// for more information.
	ServiceProperties *ServiceTypeProperties `re-export-as:"service_properties"`

	// SocketProperties contain properties specific to Socket unit types. This
	// field is only exported when the unit type is `socket`. See the
	// [systemd_SocketTypeProperties]({{< ref "socket_properties.md" >}}) docs for
	// more information.
	SocketProperties *SocketTypeProperties `re-export-as:"socket_properties"`

	// DeviceProperties contain properties specific to Device unit types. This
	// field is only exported when the unit type is `device`. See the
	// [systemd_DeviceTypeProperties]({{< ref "device_properties.md" >}}) docs for
	// more information.
	DeviceProperties *DeviceTypeProperties `re-export-as:"device_properties"`

	// MountProperties contain properties specific to Mount unit types. This field
	// is only exported when the unit type is `mount`. See the
	// [systemd_MountTypeProperties]({{< ref "mount_properties.md" >}}) docs for
	// more information.
	MountProperties *MountTypeProperties `re-export-as:"mount_properties"`

	// AutomountProperties contain properties specific to Autoumount unit types.
	// This field is only exported when the unit type is`automount`. See the
	// [systemd_AutomountTypeProperties]({{< ref "automount_properties.md" >}})
	// docs for more information.
	AutomountProperties *AutomountTypeProperties `re-export-as:"automount_properties"`

	// SwapProperties contain properties specific to Swap unit types. This field
	// is only exported when the unit type is `swap`. See the
	// [systemd_SwapTypeProperties]({{< ref "swap_properties.md" >}}) docs for
	// more information.
	SwapProperties *SwapTypeProperties `re-export-as:"swap_properties"`

	// PathProperties contain properties specific to Path unit types. This field
	// is only exported when the unit type is `path`. See the
	// [systemd_PathTypeProperties]({{< ref "path_properties.md" >}}) docs for
	// more information.
	PathProperties *PathTypeProperties `re-export-as:"path_properties"`

	// TimerProperties contain properties specific to Timer unit types. This field
	// is only exported when the unit type is `timer`. See the
	// [systemd_TimerTypeProperties]({{< ref "timer_properties.md" >}}) docs for
	// more information.
	TimerProperties *TimerTypeProperties `re-export-as:"timer_properties"`

	// SliceProperties contain properties specific to Slice unit types. This field
	// is only exported when the unit type is `slice`. See the
	// [systemd_SliceTypeProperties]({{< ref "slice_properties.md" >}}) docs for
	// more information.
	SliceProperties *SliceTypeProperties `re-export-as:"slice_properties"`

	// ScopeProperties contain properties specific to Scope unit types. This field
	// is only exported when the unit type is `scope`. See the
	// [systemd_ScopeTypeProperties]({{< ref "scope_properties.md" >}}) docs for
	// more information.
	ScopeProperties *ScopeTypeProperties `re-export-as:"scope_properties"`

	sendSignal      bool
	systemdExecutor SystemdExecutor
	hasRun          bool
}

type response struct {
	status resource.TaskStatus
	err    error
}

func wrapCall(f func() (resource.TaskStatus, error)) <-chan response {
	resp := make(chan response)
	go func() {
		st, err := f()
		resp <- response{st, err}
	}()
	return resp
}

// Check implements resource.Task
func (r *Resource) Check(ctx context.Context, _ resource.Renderer) (resource.TaskStatus, error) {
	ch := wrapCall(r.runCheck)
	select {
	case <-ctx.Done():
		return nil, errors.New("context was cancelled")
	case results := <-ch:
		return results.status, results.err
	}
}

// Apply implemnts resource.Task
func (r *Resource) Apply(ctx context.Context) (resource.TaskStatus, error) {
	ch := wrapCall(r.runApply)
	select {
	case <-ctx.Done():
		return nil, errors.New("context was cancelled")
	case results := <-ch:
		return results.status, results.err
	}
}

func (r *Resource) runCheck() (resource.TaskStatus, error) {
	status := resource.NewStatus()
	u, err := r.systemdExecutor.QueryUnit(r.Name, false)
	if err != nil {
		status.AddMessage("query unit returned an error: " + err.Error())
		return nil, err
	}
	r.populateFromUnit(u)
	if r.sendSignal && !r.hasRun {
		status.RaiseLevel(resource.StatusWillChange)
		status.AddMessage(fmt.Sprintf("Sending signal `%s` to unit", r.SignalName))
	}
	if r.Reload && !r.hasRun {
		status.RaiseLevel(resource.StatusWillChange)
		status.AddMessage("Reloading unit configuration")
		status.AddDifference("state", u.ActiveState, "reloaded", "")
	}
	switch r.State {
	case "restarted":
		status.RaiseLevel(resource.StatusWillChange)
		status.AddMessage("Restarting unit")
		status.AddDifference("state", u.ActiveState, "restarted", "")
	case "running":
		r.shouldStart(u, status)
	case "stopped":
		r.shouldStop(u, status)
	}
	r.hasRun = true
	return status, nil
}

func (r *Resource) runApply() (resource.TaskStatus, error) {
	log.WithField("Unit Name: ", r.Name).Infof("calling runApply()....")
	status := resource.NewStatus()
	tempStatus := resource.NewStatus()
	u, err := r.systemdExecutor.QueryUnit(r.Name, false)
	if err != nil {
		return nil, err
	}

	if u.ActiveState == "unknown" {
		log.Infof("unable to query information about the unit. " +
			"Making a best guess based on configured data")
		u.Name = r.Name
	}

	if r.sendSignal {
		status.AddMessage(fmt.Sprintf("Sending signal `%s` to unit", r.SignalName))
		r.systemdExecutor.SendSignal(u, Signal(r.SignalNumber))
	}
	if r.Reload {
		status.AddMessage("Reloading unit configuration")
		status.AddDifference("state", u.ActiveState, "reloaded", "")
		if err := r.systemdExecutor.ReloadUnit(u); err != nil {
			return nil, err
		}
	}

	var runstateErr error
	switch r.State {
	case "running":
		if r.shouldStart(u, tempStatus) {
			runstateErr = r.systemdExecutor.StartUnit(u)
		}
	case "stopped":
		if r.shouldStop(u, tempStatus) {
			runstateErr = r.systemdExecutor.StopUnit(u)
		}
	case "restarted":
		runstateErr = r.systemdExecutor.RestartUnit(u)
	}
	return status, runstateErr
}

// We copy data from the unit into the resource to make the UX nicer for users
// who want to access systemd information.
func (r *Resource) populateFromUnit(u *Unit) {
	r.Description = u.Description
	r.Path = u.Path
	r.Type = u.Type
	r.TypeStr = u.Type.String()
	r.Status = u.ActiveState
	r.Properties = u.Properties
	r.ServiceProperties = u.ServiceProperties
	r.SocketProperties = u.SocketProperties
	r.DeviceProperties = u.DeviceProperties
	r.MountProperties = u.MountProperties
	r.AutomountProperties = u.AutomountProperties
	r.SwapProperties = u.SwapProperties
	r.PathProperties = u.PathProperties
	r.TimerProperties = u.TimerProperties
	r.SliceProperties = u.SliceProperties
	r.ScopeProperties = u.ScopeProperties
}

func (r *Resource) shouldStart(u *Unit, st *resource.Status) bool {
	switch u.ActiveState {
	case "active":
		st.AddMessage("already running")
		return false
	case "reloading":
		st.AddMessage("unit is reloading, will re-check status during apply")
		st.RaiseLevel(resource.StatusMayChange)
		return true
	case "inactive":
		st.RaiseLevel(resource.StatusWillChange)
		st.AddDifference("state", "inactive", "active", "")
		return true
	case "failed":
		st.AddMessage("unit has failed; will attempt to restart")
		if reason, err := getFailedReason(u); err != nil {
			st.AddMessage(fmt.Sprintf("cannot determine root cause of failure: %v", err))
		} else {
			st.AddMessage(fmt.Sprintf("unit previously failed, the message was: %s", reason))
		}
		st.RaiseLevel(resource.StatusWillChange)
		st.AddDifference("state", "failed", "active", "")
		return true
	case "activating":
		st.AddMessage("unit is alread activating, will re-check status during apply")
		st.RaiseLevel(resource.StatusMayChange)
		return true
	case "deactivating":
		st.AddMessage("unit is currently deactivating, will re-check status during apply")
		st.RaiseLevel(resource.StatusMayChange)
		st.AddDifference("state", "deactivating", "active", "")
		return true
	case "unknown":
		st.AddMessage("unit was in an unknown state, will attempt to start")
		st.RaiseLevel(resource.StatusMayChange)
		st.AddDifference("state", "unknown", "active", "")
		return true
	}
	return true
}

func (r *Resource) shouldStop(u *Unit, st *resource.Status) bool {
	switch u.ActiveState {
	case "active":
		st.AddDifference("state", "active", "inactive", "")
		st.RaiseLevel(resource.StatusWillChange)
		return true
	case "reloading":
		st.AddMessage("unit is reloading; will re-check status during apply")
		st.AddDifference("state", "reloading", "inactive", "")
		st.RaiseLevel(resource.StatusMayChange)
		return true
	case "inactive":
		st.AddMessage("unit is already inactive")
		return false
	case "failed":
		st.AddMessage("unit is not running because it has failed.  Will not restart")
		if reason, err := getFailedReason(u); err != nil {
			st.AddMessage(fmt.Sprintf("cannot determine root cause of failure: %v", err))
		} else {
			st.AddMessage(fmt.Sprintf("unit previously failed, the message was: %s", reason))
		}
		return false
	case "activating":
		st.AddDifference("state", "active", "inactive", "")
		st.RaiseLevel(resource.StatusMayChange)
		return true
	case "deactivating":
		st.AddMessage("unit is deactivating.  Will re-check status during apply")
		st.RaiseLevel(resource.StatusMayChange)
		return true
	case "unknown":
		st.AddMessage("unit was in an unknown state, will attempt to stop")
		st.RaiseLevel(resource.StatusMayChange)
		st.AddDifference("state", "unknown", "inactive", "")
		return true
	}
	return true
}

func getFailedReason(u *Unit) (string, error) {
	err := errors.New("unable to determine cause of failure: no properties available")
	var reason string
	switch u.Type {
	case UnitTypeService:
		if u.ServiceProperties == nil {
			return "", err
		}
		reason = u.ServiceProperties.Result
	case UnitTypeSocket:
		if u.SocketProperties == nil {
			return "", err
		}
		reason = u.SocketProperties.Result
	case UnitTypeMount:
		if u.MountProperties == nil {
			return "", err
		}
		reason = u.MountProperties.Result
	case UnitTypeAutoMount:
		if u.AutomountProperties == nil {
			return "", err
		}
		reason = u.AutomountProperties.Result
	case UnitTypeSwap:
		if u.SwapProperties == nil {
			return "", err
		}
		reason = u.SwapProperties.Result
	case UnitTypeTimer:
		if u.TimerProperties == nil {
			return "", err
		}
		reason = u.TimerProperties.Result
	}
	switch reason {
	case "success":
		return "the unit was activated successfully", nil
	case "resources":
		return "not enough resources available to create the process", nil
	case "timeout":
		return "a timeout occurred while starting the unit", nil
	case "exit-code":
		return "unit exited with a non-zero exit code", nil
	case "signal":
		return "unit exited due to an unhandled signal", nil
	case "core-dump":
		return "unit exited and dumped core", nil
	case "watchdog":
		return "watchdog terminated the service due to slow or missing responses", nil
	case "start-limit":
		return "process has been restarted too many times", nil
	case "service-failed-permanent":
		return "continual failure of this socket", nil
	}
	return "unknown reason", nil
}
