// Mgmt
// Copyright (C) James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.
//
// Additional permission under GNU GPL version 3 section 7
//
// If you modify this program, or any covered work, by linking or combining it
// with embedded mcl code and modules (and that the embedded mcl code and
// modules which link with this program, contain a copy of their source code in
// the authoritative form) containing parts covered by the terms of any other
// license, the licensors of this program grant you additional permission to
// convey the resulting work. Furthermore, the licensors of this program grant
// the original author, James Shubin, additional permission to update this
// additional permission if he deems it necessary to achieve the goals of this
// additional permission.

//go:build !nobmc

package resources

import (
	"context"
	"fmt"
	"net"
	"net/url"
	"strconv"
	"strings"

	"github.com/purpleidea/mgmt/engine"
	"github.com/purpleidea/mgmt/engine/traits"

	bmclib "github.com/bmc-toolbox/bmclib/v2"
	"github.com/bmc-toolbox/bmclib/v2/providers/rpc"
)

func init() {
	engine.RegisterResource("bmc:power", func() engine.Res { return &BmcPowerRes{} })
}

const (
	// DefaultBmcPowerPort is the default port we try to connect on.
	DefaultBmcPowerPort = 443
)

// BmcPowerRes is a resource that manages power state of a BMC. This is usually
// used for turning computers on and off. The name value can be a big URL string
// in the form: `driver://user:pass@hostname:port` for example you may see:
// gofishs://ADMIN:hunter2@127.0.0.1:8800 to use the "https" variant of the
// gofish driver.
//
// NOTE: New drivers should either not end in "s" or at least not be identical
// to the name of another driver an "s" is added or removed to the end.
type BmcPowerRes struct {
	traits.Base // add the base methods without re-implementation

	init *engine.Init

	// Hostname to connect to. If not specified, we parse this from the
	// Name.
	Hostname string `lang:"hostname" yaml:"hostname"`

	// Port to connect to. If not specified, we parse this from the Name.
	Port int `lang:"port" yaml:"port"`

	// Username to use to connect. If not specified, we parse this from the
	// Name.
	// TODO: If the Username field is not set, should we parse from the
	// Name? It's not really part of the BMC unique identifier so maybe we
	// shouldn't use that.
	Username string `lang:"username" yaml:"username"`

	// Password to use to connect. We do NOT parse this from the Name unless
	// you set InsecurePassword to true.
	// XXX: Use mgmt magic credentials in the future.
	Password string `lang:"password" yaml:"password"`

	// InsecurePassword can be set to true to allow a password in the Name.
	InsecurePassword bool `lang:"insecure_password" yaml:"insecure_password"`

	// Driver to use, such as: "gofish" or "rpc". This is a different
	// concept than the "bmclib" driver vs provider distinction. Here we
	// just statically pick what we're using without any magic. If not
	// specified, we parse this from the Name scheme. If this ends with an
	// extra "s" then we use https instead of http.
	Driver string `lang:"driver" yaml:"driver"`

	// State of machine power. Can be "on" or "off".
	State string `lang:"state" yaml:"state"`

	driver string
	scheme string
}

// validDriver determines if we are using a valid drive. This does not include
// the magic "s" bits. This function need to be expanded as we support more
// drivers.
func (obj *BmcPowerRes) validDriver(driver string) error {
	if driver == BmcDriverRPC {
		return nil
	}
	if driver == BmcDriverGofish {
		return nil
	}

	return fmt.Errorf("unknown driver: %s", driver)
}

// getHostname returns the hostname that we want to connect to. If the Hostname
// field is set, we use that, otherwise we parse from the Name.
func (obj *BmcPowerRes) getHostname() string {
	if obj.Hostname != "" {
		return obj.Hostname
	}

	u, err := url.Parse(obj.Name())
	if err != nil || u == nil {
		return ""
	}

	// SplitHostPort splits a network address of the form "host:port",
	// "host%zone:port", "[host]:port" or "[host%zone]:port" into host or
	// host%zone and port.
	host, port, err := net.SplitHostPort(u.Host)
	if err != nil {
		return u.Host // must be a naked hostname or ip w/o port
	}
	_ = port

	return host
}

// getPort returns the port that we want to connect to. If the Port field is
// set, we use that, otherwise we parse from the Name.
//
// NOTE: We return a string since all the bmclib things usually expect a string,
// but if that gets fixed we should return an int here instead.
func (obj *BmcPowerRes) getPort() string {
	if obj.Port != 0 {
		return strconv.Itoa(obj.Port)
	}

	u, err := url.Parse(obj.Name())
	if err != nil || u == nil {
		return ""
	}

	// SplitHostPort splits a network address of the form "host:port",
	// "host%zone:port", "[host]:port" or "[host%zone]:port" into host or
	// host%zone and port.
	host, port, err := net.SplitHostPort(u.Host)
	if err != nil {
		return strconv.Itoa(DefaultBmcPowerPort) // default port
	}
	_ = host

	return port
}

// getUsername returns the username that we want to connect with. If the
// Username field is set, we use that, otherwise we parse from the Name.
// TODO: If the Username field is not set, should we parse from the Name? It's
// not really part of the BMC unique identifier so maybe we shouldn't use that.
func (obj *BmcPowerRes) getUsername() string {
	if obj.Username != "" {
		return obj.Username
	}

	u, err := url.Parse(obj.Name())
	if err != nil || u == nil || u.User == nil {
		return ""
	}

	return u.User.Username()
}

// getPassword returns the password that we want to connect with.
// XXX: Use mgmt magic credentials in the future.
func (obj *BmcPowerRes) getPassword() string {
	if obj.Password != "" || !obj.InsecurePassword {
		return obj.Password
	}
	// NOTE: We don't look at any password string from the name unless the
	// InsecurePassword field is true.

	u, err := url.Parse(obj.Name())
	if err != nil || u == nil || u.User == nil {
		return ""
	}

	password, ok := u.User.Password()
	if !ok {
		return ""
	}

	return password
}

// getRawDriver returns the raw magic driver string. If the Driver field is set,
// we use that, otherwise we parse from the Name. This version may include the
// magic "s" at the end.
func (obj *BmcPowerRes) getRawDriver() string {
	if obj.Driver != "" {
		return obj.Driver
	}

	u, err := url.Parse(obj.Name())
	if err != nil || u == nil {
		return ""
	}

	return u.Scheme
}

// getDriverAndScheme figures out which driver and scheme we want to use.
func (obj *BmcPowerRes) getDriverAndScheme() (string, string, error) {
	driver := obj.getRawDriver()
	err := obj.validDriver(driver)
	if err == nil {
		return driver, "http", nil
	}

	driver = strings.TrimSuffix(driver, BmcDriverSecureSuffix)
	if err := obj.validDriver(driver); err == nil {
		return driver, "https", nil
	}

	return "", "", err // return the first error
}

// getDriver returns the actual driver that we want to connect with. If the
// Driver field is set, we use that, otherwise we parse from the Name. This
// version does NOT include the magic "s" at the end.
func (obj *BmcPowerRes) getDriver() string {
	return obj.driver
}

// getScheme figures out which scheme we want to use.
func (obj *BmcPowerRes) getScheme() string {
	return obj.scheme
}

// Default returns some sensible defaults for this resource.
func (obj *BmcPowerRes) Default() engine.Res {
	return &BmcPowerRes{}
}

// Validate if the params passed in are valid data.
func (obj *BmcPowerRes) Validate() error {
	// XXX: Force polling until we have real events...
	if obj.MetaParams().Poll == 0 {
		return fmt.Errorf("events are not yet supported, use polling")
	}

	if obj.getHostname() == "" {
		return fmt.Errorf("need a Hostname")
	}
	//if obj.getUsername() == "" {
	//	return fmt.Errorf("need a Username")
	//}

	if obj.getRawDriver() == "" {
		return fmt.Errorf("need a Driver")
	}
	if _, _, err := obj.getDriverAndScheme(); err != nil {
		return err
	}

	return nil
}

// Init runs some startup code for this resource.
func (obj *BmcPowerRes) Init(init *engine.Init) error {
	obj.init = init // save for later

	driver, scheme, err := obj.getDriverAndScheme()
	if err != nil {
		// programming error (we checked in Validate)
		return err
	}
	obj.driver = driver
	obj.scheme = scheme

	return nil
}

// Cleanup is run by the engine to clean up after the resource is done.
func (obj *BmcPowerRes) Cleanup() error {
	return nil
}

// client builds the bmclib client. The API to build it is complicated.
func (obj *BmcPowerRes) client() *bmclib.Client {
	// NOTE: The bmclib API is weird, you can't put the port in this string!
	u := fmt.Sprintf("%s://%s", obj.getScheme(), obj.getHostname())

	uPort := u
	if p := obj.getPort(); p != "" {
		uPort = u + ":" + p
	}

	opts := []bmclib.Option{}

	if obj.getDriver() == BmcDriverRPC {
		opts = append(opts, bmclib.WithRPCOpt(rpc.Provider{
			// NOTE: The main API cannot take a port, but here we do!
			ConsumerURL: uPort,
		}))
	}

	if p := obj.getPort(); p != "" {
		switch obj.getDriver() {
		case BmcDriverRPC:
			// TODO: ???

		case BmcDriverGofish:
			// XXX: Why doesn't this accept an int?
			opts = append(opts, bmclib.WithRedfishPort(p))

		//case BmcDriverOpenbmc:
		//	// XXX: Why doesn't this accept an int?
		//	opts = append(opts, openbmc.WithPort(p))

		default:
			// TODO: error or pass through?
			obj.init.Logf("unhandled driver: %s", obj.getDriver())
		}
	}

	client := bmclib.NewClient(u, obj.getUsername(), obj.Password, opts...)
	if obj.getDriver() == BmcDriverGofish {
		client = client.PreferProtocol("redfish") // joel said to do this
	}

	if obj.getDriver() != "" && obj.getDriver() != BmcDriverRPC {
		client = client.For(obj.getDriver()) // limit to this provider
	}

	return client
}

// Watch is the primary listener for this resource and it outputs events.
func (obj *BmcPowerRes) Watch(ctx context.Context) error {
	if err := obj.init.Event(ctx); err != nil {
		return err
	}

	select {
	case <-ctx.Done(): // closed by the engine to signal shutdown
	}

	return ctx.Err()
}

// CheckApply method for BmcPower resource. Does nothing, returns happy!
func (obj *BmcPowerRes) CheckApply(ctx context.Context, apply bool) (bool, error) {
	bs := bmcStateReserve(obj.Name()) // get a handle to the shared bmc state
	defer bs.Release()

	// We only want one resource touching the bmc at a time.
	bs.Lock()
	defer bs.Unlock()

	client := obj.client()

	if err := client.Open(ctx); err != nil {
		return false, err
	}
	defer client.Close(ctx) // (err error)

	if obj.init.Debug {
		obj.init.Logf("connected ok")
	}

	state, err := client.GetPowerState(ctx)
	if err != nil {
		return false, err
	}
	state = strings.ToLower(state) // normalize
	obj.init.Logf("get state: %s", state)

	if obj.State == state {
		return true, nil
	}

	if !apply {
		return false, nil
	}

	// TODO: should this be "On" and "Off"? Does case matter?
	ok, err := client.SetPowerState(ctx, obj.State)
	if err != nil {
		return false, err
	}
	if !ok {
		// TODO: When is this ever false?
	}
	obj.init.Logf("set state: %s", obj.State)

	return false, nil
}

// Cmp compares two resources and returns an error if they are not equivalent.
func (obj *BmcPowerRes) Cmp(r engine.Res) error {
	// we can only compare BmcPowerRes to others of the same resource kind
	res, ok := r.(*BmcPowerRes)
	if !ok {
		return fmt.Errorf("not a %s", obj.Kind())
	}

	if obj.Hostname != res.Hostname {
		return fmt.Errorf("the Hostname differs")
	}
	if obj.Port != res.Port {
		return fmt.Errorf("the Port differs")
	}
	if obj.Username != res.Username {
		return fmt.Errorf("the Username differs")
	}
	if obj.Password != res.Password {
		return fmt.Errorf("the Password differs")
	}
	if obj.InsecurePassword != res.InsecurePassword {
		return fmt.Errorf("the InsecurePassword differs")
	}

	if obj.Driver != res.Driver {
		return fmt.Errorf("the Driver differs")
	}
	if obj.State != res.State {
		return fmt.Errorf("the State differs")
	}

	return nil
}

// UnmarshalYAML is the custom unmarshal handler for this struct. It is
// primarily useful for setting the defaults.
func (obj *BmcPowerRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
	type rawRes BmcPowerRes // indirection to avoid infinite recursion

	def := obj.Default()          // get the default
	res, ok := def.(*BmcPowerRes) // put in the right format
	if !ok {
		return fmt.Errorf("could not convert to BmcPowerRes")
	}
	raw := rawRes(*res) // convert; the defaults go here

	if err := unmarshal(&raw); err != nil {
		return err
	}

	*obj = BmcPowerRes(raw) // restore from indirection with type conversion!
	return nil
}
