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

package resources

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

	"github.com/purpleidea/mgmt/engine"
	"github.com/purpleidea/mgmt/engine/resources/http_server_ui/common"
	"github.com/purpleidea/mgmt/engine/traits"
	"github.com/purpleidea/mgmt/util/errwrap"
)

const (
	httpServerUIInputKind = httpServerUIKind + ":input"

	httpServerUIInputStoreKey         = "key"
	httpServerUIInputStoreSchemeLocal = "local"
	httpServerUIInputStoreSchemeWorld = "world"

	httpServerUIInputTypeText  = common.HTTPServerUIInputTypeText  // "text"
	httpServerUIInputTypeRange = common.HTTPServerUIInputTypeRange // "range"
)

func init() {
	engine.RegisterResource(httpServerUIInputKind, func() engine.Res { return &HTTPServerUIInputRes{} })
}

var _ HTTPServerUIGroupableRes = &HTTPServerUIInputRes{} // compile time check

// HTTPServerUIInputRes is a form element that exists within a http:server:ui
// resource, which exists within an http server. The name is used as the unique
// id of the field, unless the id field is specified, and in that case it is
// used instead. The way this works is that it autogroups at runtime with an
// existing http:server:ui resource, and in doing so makes the form field
// associated with this resource available as part of that ui which is itself
// grouped and served from the http server resource.
type HTTPServerUIInputRes struct {
	traits.Base      // add the base methods without re-implementation
	traits.Edgeable  // XXX: add autoedge support
	traits.Groupable // can be grouped into HTTPServerUIRes
	traits.Sendable

	init *engine.Init

	// Path is the name of the http ui resource to group this into. If it is
	// omitted, and there is only a single http ui resource, then it will be
	// grouped into it automatically. If there is more than one main http ui
	// resource being used, then the grouping behaviour is *undefined* when
	// this is not specified, and it is not recommended to leave this blank!
	Path string `lang:"path" yaml:"path"`

	// ID is the unique id for this element. It is used in form fields and
	// should not be a private identifier. It must be unique within a given
	// http ui.
	ID string `lang:"id" yaml:"id"`

	// Value is the default value to use for the form field. If you change
	// it, then the resource graph will change and we'll rebuild and have
	// the new value visible. You can use either this or the Store field.
	// XXX: If we ever add our resource mutate API, we might not need to
	// swap to a new resource graph, and maybe Store is not needed?
	Value string `lang:"value" yaml:"value"`

	// Store the data in this source. It will also read in a default value
	// from there if one is present. It will watch it for changes as well,
	// and update the displayed value if it's changed from another source.
	// This cannot be used at the same time as the Value field.
	Store string `lang:"store" yaml:"store"`

	// Type specifies the type of input field this is, and some information
	// about it.
	// XXX: come up with a format such as "multiline://?max=60&style=foo"
	Type string `lang:"type" yaml:"type"`

	// Sort is a string that you can use to determine the global sorted
	// display order of all the elements in a ui.
	Sort string `lang:"sort" yaml:"sort"`

	scheme        string        // the scheme we're using with Store, cached for later
	key           string        // the key we're using with Store, cached for later
	typeURL       *url.URL      // the type data, cached for later
	typeURLValues url.Values    // the type data, cached for later
	last          *string       // the last value we sent
	value         string        // what we've last received from SetValue
	storeEvent    bool          // did a store event happen?
	mutex         *sync.Mutex   // guards storeEvent and value
	event         chan struct{} // local event that the setValue sends
}

// Default returns some sensible defaults for this resource.
func (obj *HTTPServerUIInputRes) Default() engine.Res {
	return &HTTPServerUIInputRes{
		Type: "text://",
	}
}

// Validate checks if the resource data structure was populated correctly.
func (obj *HTTPServerUIInputRes) Validate() error {
	if obj.GetID() == "" {
		return fmt.Errorf("empty id")
	}

	if obj.Value != "" && obj.Store != "" {
		return fmt.Errorf("may only use either Value or Store")
	}

	if obj.Value != "" {
		if err := obj.checkValue(obj.Value); err != nil {
			return errwrap.Wrapf(err, "the Value field is invalid")
		}
	}

	if obj.Store != "" {
		// XXX: check the URI format
	}

	return nil
}

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

	u, err := url.Parse(obj.Type)
	if err != nil {
		return err
	}
	if u == nil {
		return fmt.Errorf("can't parse Type")
	}
	if u.Scheme != httpServerUIInputTypeText && u.Scheme != httpServerUIInputTypeRange {
		return fmt.Errorf("unknown scheme: %s", u.Scheme)
	}
	values, err := url.ParseQuery(u.RawQuery)
	if err != nil {
		return err
	}
	obj.typeURL = u
	obj.typeURLValues = values

	if obj.Store != "" {
		u, err := url.Parse(obj.Store)
		if err != nil {
			return err
		}
		if u == nil {
			return fmt.Errorf("can't parse Store")
		}
		if u.Scheme != httpServerUIInputStoreSchemeLocal && u.Scheme != httpServerUIInputStoreSchemeWorld {
			return fmt.Errorf("unknown scheme: %s", u.Scheme)
		}
		values, err := url.ParseQuery(u.RawQuery)
		if err != nil {
			return err
		}

		obj.scheme = u.Scheme // cache for later
		obj.key = obj.Name()  // default

		x, exists := values[httpServerUIInputStoreKey]
		if exists && len(x) > 0 && x[0] != "" { // ignore absent or broken keys
			obj.key = x[0]
		}
	}

	// populate our obj.value cache somehow, so we don't mutate obj.Value
	obj.value = obj.Value // copy
	obj.mutex = &sync.Mutex{}
	obj.event = make(chan struct{}, 1) // buffer to avoid blocks or deadlock

	return nil
}

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

// getKey returns the key to be used for this resource. If the Store field is
// specified, it will use that parsed part, otherwise it uses the Name.
func (obj *HTTPServerUIInputRes) getKey() string {
	if obj.Store != "" {
		return obj.key
	}

	return obj.Name()
}

// ParentName is used to limit which resources autogroup into this one. If it's
// empty then it's ignored, otherwise it must match the Name of the parent to
// get grouped.
func (obj *HTTPServerUIInputRes) ParentName() string {
	return obj.Path
}

// GetKind returns the kind of this resource.
func (obj *HTTPServerUIInputRes) GetKind() string {
	// NOTE: We don't *need* to return such a specific string, and "input"
	// would be enough, but we might as well use this because we have it.
	return httpServerUIInputKind
}

// GetID returns the actual ID we respond to. When ID is not specified, we use
// the Name.
func (obj *HTTPServerUIInputRes) GetID() string {
	if obj.ID != "" {
		return obj.ID
	}
	return obj.Name()
}

// SetValue stores the new value field that was obtained from submitting the
// form. This receives the raw, unsafe value that you must validate first.
func (obj *HTTPServerUIInputRes) SetValue(ctx context.Context, vs []string) error {
	if len(vs) != 1 {
		return fmt.Errorf("unexpected length of %d", len(vs))
	}
	value := vs[0]

	if err := obj.checkValue(value); err != nil {
		return err
	}

	obj.mutex.Lock()
	err := obj.setValue(ctx, value) // also sends an event
	obj.mutex.Unlock()
	return err
}

// setValue is the helper version where the caller must provide the mutex.
func (obj *HTTPServerUIInputRes) setValue(ctx context.Context, val string) error {
	obj.value = val

	select {
	case obj.event <- struct{}{}:
	default:
	}

	return nil
}

func (obj *HTTPServerUIInputRes) checkValue(value string) error {
	// XXX: validate based on obj.Type
	// XXX: validate what kind of values are allowed, probably no \n, etc...
	return nil
}

// GetValue gets a string representation for the form value, that we'll use in
// our html form.
func (obj *HTTPServerUIInputRes) GetValue(ctx context.Context) (string, error) {
	obj.mutex.Lock()
	defer obj.mutex.Unlock()

	if obj.storeEvent {
		val, exists, err := obj.storeGet(ctx, obj.getKey())
		if err != nil {
			return "", errwrap.Wrapf(err, "error during get")
		}
		if !exists {
			return "", nil // default
		}
		return val, nil
	}

	return obj.value, nil
}

// GetType returns a map that you can use to build the input field in the ui.
func (obj *HTTPServerUIInputRes) GetType() map[string]string {
	m := make(map[string]string)

	if obj.typeURL.Scheme == httpServerUIInputTypeRange {
		m = obj.rangeGetType()
	}

	m[common.HTTPServerUIInputType] = obj.typeURL.Scheme

	return m
}

func (obj *HTTPServerUIInputRes) rangeGetType() map[string]string {
	m := make(map[string]string)
	base := 10
	bits := 64

	if sa, exists := obj.typeURLValues[common.HTTPServerUIInputTypeRangeMin]; exists && len(sa) > 0 {
		if x, err := strconv.ParseInt(sa[0], base, bits); err == nil {
			m[common.HTTPServerUIInputTypeRangeMin] = strconv.FormatInt(x, base)
		}
	}
	if sa, exists := obj.typeURLValues[common.HTTPServerUIInputTypeRangeMax]; exists && len(sa) > 0 {
		if x, err := strconv.ParseInt(sa[0], base, bits); err == nil {
			m[common.HTTPServerUIInputTypeRangeMax] = strconv.FormatInt(x, base)
		}
	}
	if sa, exists := obj.typeURLValues[common.HTTPServerUIInputTypeRangeStep]; exists && len(sa) > 0 {
		if x, err := strconv.ParseInt(sa[0], base, bits); err == nil {
			m[common.HTTPServerUIInputTypeRangeStep] = strconv.FormatInt(x, base)
		}
	}

	return m
}

// GetSort returns a string that you can use to determine the global sorted
// display order of all the elements in a ui.
func (obj *HTTPServerUIInputRes) GetSort() string {
	return obj.Sort
}

// Watch is the primary listener for this resource and it outputs events. This
// particular one does absolutely nothing but block until we've received a done
// signal.
func (obj *HTTPServerUIInputRes) Watch(ctx context.Context) error {
	if obj.Store != "" && obj.scheme == httpServerUIInputStoreSchemeLocal {
		return obj.localWatch(ctx)
	}
	if obj.Store != "" && obj.scheme == httpServerUIInputStoreSchemeWorld {
		return obj.worldWatch(ctx)
	}

	if err := obj.init.Event(ctx); err != nil {
		return err
	}

	// XXX: do we need to watch on obj.event for normal .Value stuff?

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

	return ctx.Err()
}

func (obj *HTTPServerUIInputRes) localWatch(ctx context.Context) error {
	ctx, cancel := context.WithCancel(ctx)
	defer cancel()

	ch, err := obj.init.Local.ValueWatch(ctx, obj.getKey()) // get possible events!
	if err != nil {
		return errwrap.Wrapf(err, "error during watch")
	}

	if err := obj.init.Event(ctx); err != nil {
		return err
	}

	for {
		select {
		case _, ok := <-ch:
			if !ok { // channel shutdown
				return nil
			}
			obj.mutex.Lock()
			obj.storeEvent = true
			obj.mutex.Unlock()

		case <-obj.event:

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

		if obj.init.Debug {
			obj.init.Logf("event!")
		}
		if err := obj.init.Event(ctx); err != nil {
			return err
		}
	}

}

func (obj *HTTPServerUIInputRes) worldWatch(ctx context.Context) error {
	ctx, cancel := context.WithCancel(ctx)
	defer cancel()

	ch, err := obj.init.World.StrWatch(ctx, obj.getKey()) // get possible events!
	if err != nil {
		return errwrap.Wrapf(err, "error during watch")
	}

	if err := obj.init.Event(ctx); err != nil {
		return err
	}

	for {
		select {
		case err, ok := <-ch:
			if !ok { // channel shutdown
				return nil
			}
			if err != nil {
				return errwrap.Wrapf(err, "unknown %s watcher error", obj)
			}
			obj.mutex.Lock()
			obj.storeEvent = true
			obj.mutex.Unlock()

		case <-obj.event:

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

		if obj.init.Debug {
			obj.init.Logf("event!")
		}
		if err := obj.init.Event(ctx); err != nil {
			return err
		}
	}

}

// CheckApply performs the send/recv portion of this autogrouped resources. That
// can fail, but only if the send portion fails for some reason. If we're using
// the Store feature, then it also reads and writes to and from that store.
func (obj *HTTPServerUIInputRes) CheckApply(ctx context.Context, apply bool) (bool, error) {
	if obj.init.Debug {
		obj.init.Logf("CheckApply")
	}

	// If we're in ".Value" mode, we want to look at the incoming value, and
	// send it onwards. This function mostly exists as a stub in this case.
	// The private value gets set by obj.SetValue from the http:server:ui
	// parent. If we're in ".Store" mode, then we're reconciling between the
	// "World" and the http:server:ui "Web".

	if obj.Store != "" {
		return obj.storeCheckApply(ctx, apply)
	}

	return obj.valueCheckApply(ctx, apply)

}

func (obj *HTTPServerUIInputRes) valueCheckApply(ctx context.Context, apply bool) (bool, error) {

	obj.mutex.Lock()
	value := obj.value // gets set by obj.SetValue
	obj.mutex.Unlock()

	if obj.last != nil && *obj.last == value {
		if err := obj.init.Send(&HTTPServerUIInputSends{
			Value: &value,
		}); err != nil {
			return false, err
		}
		return true, nil // expected value has already been sent
	}

	if !apply {
		if err := obj.init.Send(&HTTPServerUIInputSends{
			Value: &value, // XXX: arbitrary since we're in noop mode
		}); err != nil {
			return false, err
		}
		return false, nil
	}

	s := value    // copy
	obj.last = &s // cache

	// XXX: This is getting called twice, what's the bug?
	obj.init.Logf("sending: %s", value)

	// send
	if err := obj.init.Send(&HTTPServerUIInputSends{
		Value: &value,
	}); err != nil {
		return false, err
	}

	return false, nil
	//return true, nil // always succeeds, with nothing to do!
}

// storeCheckApply is a tricky function where we attempt to reconcile the state
// between a third-party changing the value in the World database, and a recent
// "http:server:ui" change by an end user. Basically whoever runs last is the
// "right" value that we want to use. We know who sent the event from reading
// the storeEvent variable, and if it was the World, we want to cache it
// locally, and if it was the Web, then we want to push it up to the store.
func (obj *HTTPServerUIInputRes) storeCheckApply(ctx context.Context, apply bool) (bool, error) {

	v1, exists, err := obj.storeGet(ctx, obj.getKey())
	if err != nil {
		return false, errwrap.Wrapf(err, "error during get")
	}

	obj.mutex.Lock()
	v2 := obj.value // gets set by obj.SetValue
	storeEvent := obj.storeEvent
	obj.storeEvent = false // reset it
	obj.mutex.Unlock()

	if exists && v1 == v2 { // both sides are happy
		if err := obj.init.Send(&HTTPServerUIInputSends{
			Value: &v2,
		}); err != nil {
			return false, err
		}
		return true, nil
	}

	if !apply {
		if err := obj.init.Send(&HTTPServerUIInputSends{
			Value: &v2, // XXX: arbitrary since we're in noop mode
		}); err != nil {
			return false, err
		}
		return false, nil
	}

	obj.mutex.Lock()
	if storeEvent { // event from World, pull down the value
		err = obj.setValue(ctx, v1) // also sends an event
	}
	value := obj.value
	obj.mutex.Unlock()
	if err != nil {
		return false, err
	}

	if !exists || !storeEvent { // event from web, push up the value
		if err := obj.storeSet(ctx, obj.getKey(), value); err != nil {
			return false, errwrap.Wrapf(err, "error during set")
		}
	}

	obj.init.Logf("sending: %s", value)

	// send
	if err := obj.init.Send(&HTTPServerUIInputSends{
		Value: &value,
	}); err != nil {
		return false, err
	}

	return false, nil
}

func (obj *HTTPServerUIInputRes) storeGet(ctx context.Context, key string) (string, bool, error) {
	if obj.Store != "" && obj.scheme == httpServerUIInputStoreSchemeLocal {
		val, err := obj.init.Local.ValueGet(ctx, key)
		if err != nil {
			return "", false, err // real error
		}
		if val == nil { // if val is nil, and no error then it doesn't exist
			return "", false, nil // val doesn't exist
		}
		s, ok := val.(string)
		if !ok {
			// TODO: support different types perhaps?
			return "", false, fmt.Errorf("not a string") // real error
		}
		return s, true, nil
	}

	if obj.Store != "" && obj.scheme == httpServerUIInputStoreSchemeWorld {
		val, err := obj.init.World.StrGet(ctx, key)
		if err != nil && obj.init.World.StrIsNotExist(err) {
			return "", false, nil // val doesn't exist
		}
		if err != nil {
			return "", false, err // real error
		}
		return val, true, nil
	}

	return "", false, nil // something else
}

func (obj *HTTPServerUIInputRes) storeSet(ctx context.Context, key, val string) error {

	if obj.Store != "" && obj.scheme == httpServerUIInputStoreSchemeLocal {
		return obj.init.Local.ValueSet(ctx, key, val)
	}

	if obj.Store != "" && obj.scheme == httpServerUIInputStoreSchemeWorld {
		return obj.init.World.StrSet(ctx, key, val)
	}

	return nil // something else
}

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

	if obj.Path != res.Path {
		return fmt.Errorf("the Path differs")
	}
	if obj.ID != res.ID {
		return fmt.Errorf("the ID differs")
	}
	if obj.Value != res.Value {
		return fmt.Errorf("the Value differs")
	}
	if obj.Store != res.Store {
		return fmt.Errorf("the Store differs")
	}
	if obj.Type != res.Type {
		return fmt.Errorf("the Type differs")
	}
	if obj.Sort != res.Sort {
		return fmt.Errorf("the Sort differs")
	}

	return nil
}

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

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

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

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

// HTTPServerUIInputSends is the struct of data which is sent after a successful
// Apply.
type HTTPServerUIInputSends struct {
	// Value is the text element value being sent.
	Value *string `lang:"value"`
}

// Sends represents the default struct of values we can send using Send/Recv.
func (obj *HTTPServerUIInputRes) Sends() interface{} {
	return &HTTPServerUIInputSends{
		Value: nil,
	}
}
