// Copyright 2012-present Oliver Eilhard. All rights reserved.
// Use of this source code is governed by a MIT-license.
// See http://olivere.mit-license.org/license.txt for details.

package elastic

import (
	"encoding/json"
	"errors"
	"fmt"
	"strings"
)

// Script holds all the parameters necessary to compile or find in cache
// and then execute a script.
//
// See https://www.elastic.co/guide/en/elasticsearch/reference/7.0/modules-scripting.html
// for details of scripting.
type Script struct {
	script string
	typ    string
	lang   string
	params map[string]interface{}
}

// NewScript creates and initializes a new Script. By default, it is of
// type "inline". Use NewScriptStored for a stored script (where type is "id").
func NewScript(script string) *Script {
	return &Script{
		script: script,
		typ:    "inline",
		params: make(map[string]interface{}),
	}
}

// NewScriptInline creates and initializes a new inline script, i.e. code.
func NewScriptInline(script string) *Script {
	return NewScript(script).Type("inline")
}

// NewScriptStored creates and initializes a new stored script.
func NewScriptStored(script string) *Script {
	return NewScript(script).Type("id")
}

// Script is either the cache key of the script to be compiled/executed
// or the actual script source code for inline scripts. For indexed
// scripts this is the id used in the request. For file scripts this is
// the file name.
func (s *Script) Script(script string) *Script {
	s.script = script
	return s
}

// Type sets the type of script: "inline" or "id".
func (s *Script) Type(typ string) *Script {
	s.typ = typ
	return s
}

// Lang sets the language of the script. The default scripting language
// is Painless ("painless").
// See https://www.elastic.co/guide/en/elasticsearch/reference/7.0/modules-scripting.html
// for details.
func (s *Script) Lang(lang string) *Script {
	s.lang = lang
	return s
}

// Param adds a key/value pair to the parameters that this script will be executed with.
func (s *Script) Param(name string, value interface{}) *Script {
	if s.params == nil {
		s.params = make(map[string]interface{})
	}
	s.params[name] = value
	return s
}

// Params sets the map of parameters this script will be executed with.
func (s *Script) Params(params map[string]interface{}) *Script {
	s.params = params
	return s
}

// Source returns the JSON serializable data for this Script.
func (s *Script) Source() (interface{}, error) {
	if s.typ == "" && s.lang == "" && len(s.params) == 0 {
		return s.script, nil
	}
	source := make(map[string]interface{})
	// Beginning with 6.0, the type can only be "source" or "id"
	if s.typ == "" || s.typ == "inline" {
		src, err := s.rawScriptSource(s.script)
		if err != nil {
			return nil, err
		}
		source["source"] = src
	} else {
		source["id"] = s.script
	}
	if s.lang != "" {
		source["lang"] = s.lang
	}
	if len(s.params) > 0 {
		source["params"] = s.params
	}
	return source, nil
}

// rawScriptSource returns an embeddable script. If it uses a short
// script form, e.g. "ctx._source.likes++" (without the quotes), it
// is quoted. Otherwise it returns the raw script that will be directly
// embedded into the JSON data.
func (s *Script) rawScriptSource(script string) (interface{}, error) {
	v := strings.TrimSpace(script)
	if !strings.HasPrefix(v, "{") && !strings.HasPrefix(v, `"`) {
		v = fmt.Sprintf("%q", v)
	}
	raw := json.RawMessage(v)
	return &raw, nil
}

// -- Script Field --

// ScriptField is a single script field.
type ScriptField struct {
	FieldName string // name of the field

	script        *Script
	ignoreFailure *bool // used in e.g. ScriptSource
}

// NewScriptField creates and initializes a new ScriptField.
func NewScriptField(fieldName string, script *Script) *ScriptField {
	return &ScriptField{FieldName: fieldName, script: script}
}

// IgnoreFailure indicates whether to ignore failures. It is used
// in e.g. ScriptSource.
func (f *ScriptField) IgnoreFailure(ignore bool) *ScriptField {
	f.ignoreFailure = &ignore
	return f
}

// Source returns the serializable JSON for the ScriptField.
func (f *ScriptField) Source() (interface{}, error) {
	if f.script == nil {
		return nil, errors.New("ScriptField expects script")
	}
	source := make(map[string]interface{})
	src, err := f.script.Source()
	if err != nil {
		return nil, err
	}
	source["script"] = src
	if v := f.ignoreFailure; v != nil {
		source["ignore_failure"] = *v
	}
	return source, nil
}