package kolide

import (
	"database/sql/driver"
	"encoding/json"
	"fmt"
	"strings"

	"golang.org/x/net/context"
)

// OptionStore interface describes methods to access datastore
type OptionStore interface {
	// SaveOptions transactional write of options to storage.  If one or more
	// values fails validation none of the writes will succeed. Note only option
	// values are written.  Other option fields are created in migration and do
	// not change. Attempting to write ReadOnly options will cause an error.
	SaveOptions(opts []Option) error
	// Options returns all options
	ListOptions() ([]Option, error)
	// Option return an option by ID
	Option(id uint) (*Option, error)
	// OptionByName returns an option uniquely identified by name
	OptionByName(name string) (*Option, error)
	// GetOsqueryConfigOptions returns options in a format that will be the options
	// section of osquery configuration
	GetOsqueryConfigOptions() (map[string]interface{}, error)
}

// OptionService interface describes methods that operate on osquery options
type OptionService interface {
	// GetOptions retrieves all options
	GetOptions(ctx context.Context) ([]Option, error)
	// ModifyOptions will change values of the options in OptionRequest.  Note
	// passing ReadOnly options will cause an error.
	ModifyOptions(ctx context.Context, req OptionRequest) ([]Option, error)
}

const (
	ReadOnly    = true
	NotReadOnly = !ReadOnly
)

// OptionType defines the type of the value assigned to an option
type OptionType int

const (
	OptionTypeString OptionType = iota
	OptionTypeInt
	OptionTypeBool
)

// String values that map from JSON to OptionType
const (
	optionTypeString = "string"
	optionTypeInt    = "int"
	optionTypeBool   = "bool"
)

// MarshalJSON marshals option type to strings
func (ot OptionType) MarshalJSON() ([]byte, error) {
	return []byte(fmt.Sprintf(`"%s"`, ot)), nil
}

// UnmarshalJSON converts json to OptionType
func (ot *OptionType) UnmarshalJSON(b []byte) error {
	switch typ := string(b); strings.Trim(typ, `"`) {
	case optionTypeString:
		*ot = OptionTypeString
	case optionTypeBool:
		*ot = OptionTypeBool
	case optionTypeInt:
		*ot = OptionTypeInt
	default:
		return fmt.Errorf("unsupported option type '%s'", typ)
	}
	return nil
}

// String is used to marshal OptionType to human readable strings used in JSON payloads
func (ot OptionType) String() string {
	switch ot {
	case OptionTypeString:
		return optionTypeString
	case OptionTypeInt:
		return optionTypeInt
	case OptionTypeBool:
		return optionTypeBool
	default:
		panic("stringer not implemented for OptionType")
	}
}

// OptionValue supports Valuer and Scan interfaces for reading and writing
// to the database, and also JSON marshaling
type OptionValue struct {
	Val interface{}
}

// Value is called by the DB driver.  Note that we store data as JSON
// types, so we can use the JSON marshaller to assign the appropriate
// type when we fetch it from the db
func (ov OptionValue) Value() (dv driver.Value, err error) {
	return json.Marshal(ov.Val)
}

// Scan takes db string and turns it into an option type
func (ov *OptionValue) Scan(src interface{}) error {
	return json.Unmarshal(src.([]byte), &ov.Val)
}

// MarshalJSON implements the json.Marshaler interface
func (ov OptionValue) MarshalJSON() ([]byte, error) {
	return json.Marshal(ov.Val)
}

// UnmarshalJSON implements the json.Unmarshaler interface
func (ov *OptionValue) UnmarshalJSON(b []byte) error {
	return json.Unmarshal(b, &ov.Val)
}

// Option represents a possible osquery confguration option
// See https://osquery.readthedocs.io/en/stable/deployment/configuration/
type Option struct {
	// ID unique identifier for option assigned by the dbms
	ID uint `json:"id"`
	// Name of the option which must be unique
	Name string `json:"name"`
	// Type of value for the option
	Type OptionType `json:"type"`
	// Value of the option which may be nil, bool, int, or string.
	Value OptionValue `json:"value"`
	// ReadOnly if true, this option is required for Kolide to function
	// properly and cannot be modified by the end user
	ReadOnly bool `json:"read_only" db:"read_only"`
}

// SetValue sets the value associated with the option
func (opt *Option) SetValue(v interface{}) {
	opt.Value.Val = v
}

func (opt *Option) SameType(compare interface{}) bool {
	switch compare.(type) {
	case float64:
		return opt.Type == OptionTypeInt
	case string:
		return opt.Type == OptionTypeString
	case bool:
		return opt.Type == OptionTypeBool
	default:
		return false
	}
}

// OptionSet returns true if the option has a value assigned to it
func (opt *Option) OptionSet() bool {
	return opt.Value.Val != nil
}

// GetValue returns the value associated with the option
func (opt Option) GetValue() interface{} {
	return opt.Value.Val
}

// OptionRequest contains options that are passed to modify options requests.
type OptionRequest struct {
	Options []Option `json:"options"`
}
