// Copyright (c) 2019, Google Inc.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

//go:build interactive
// +build interactive

package main

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	neturl "net/url"
	"os"
	"os/exec"
	"os/signal"
	"path/filepath"
	"reflect"
	"strconv"
	"strings"
	"syscall"

	"boringssl.googlesource.com/boringssl/util/fipstools/acvp/acvptool/acvp"
	"golang.org/x/crypto/ssh/terminal"
)

const interactiveModeSupported = true

func updateTerminalSize(term *terminal.Terminal) {
	width, height, err := terminal.GetSize(0)
	if err != nil {
		return
	}
	term.SetSize(width, height)
}

func skipWS(node *node32) *node32 {
	for ; node != nil && node.pegRule == ruleWS; node = node.next {
	}
	return node
}

func assertNodeType(node *node32, rule pegRule) {
	if node.pegRule != rule {
		panic(fmt.Sprintf("expected %q, found %q", rul3s[rule], rul3s[node.pegRule]))
	}
}

type Object interface {
	String() (string, error)
	Index(string) (Object, error)
	Search(acvp.Query) (Object, error)
	Action(action string, args []string) error
}

type ServerObjectSet struct {
	env          *Env
	name         string
	searchKeys   map[string][]acvp.Relation
	resultType   reflect.Type
	subObjects   map[string]func(*Env, string) (Object, error)
	canEnumerate bool
}

func (set ServerObjectSet) String() (string, error) {
	if !set.canEnumerate {
		return "[object set " + set.name + "]", nil
	}

	data := reflect.New(reflect.SliceOf(set.resultType)).Interface()
	if err := set.env.server.GetPaged(data, "acvp/v1/"+set.name, nil); err != nil {
		return "", err
	}
	ret, err := json.MarshalIndent(data, "", "  ")
	return string(ret), err
}

func (set ServerObjectSet) Index(indexStr string) (Object, error) {
	index, err := strconv.ParseUint(indexStr, 0, 64)
	if err != nil {
		return nil, fmt.Errorf("object set indexes must be unsigned integers, trying to parse %q failed: %s", indexStr, err)
	}
	return ServerObject{&set, index}, nil
}

func (set ServerObjectSet) Search(condition acvp.Query) (Object, error) {
	if set.searchKeys == nil {
		return nil, errors.New("this object set cannot be searched")
	}

	for _, conj := range condition {
	NextCondition:
		for _, cond := range conj {
			allowed, ok := set.searchKeys[cond.Param]
			if !ok {
				return nil, fmt.Errorf("search key %q not valid for this object set", cond.Param)
			}

			for _, rel := range allowed {
				if rel == cond.Relation {
					continue NextCondition
				}
			}

			return nil, fmt.Errorf("search key %q cannot be used with relation %q", cond.Param, cond.Relation.String())
		}
	}

	return Search{ServerObjectSet: set, query: condition}, nil
}

func (set ServerObjectSet) Action(action string, args []string) error {
	switch action {
	default:
		return fmt.Errorf("unknown action %q", action)

	case "new":
		if len(args) != 0 {
			return fmt.Errorf("found %d arguments but %q takes none", len(args), action)
		}

		newContents, err := edit("")
		if err != nil {
			return err
		}

		if strings.TrimSpace(string(newContents)) == "" {
			io.WriteString(set.env.term, "Resulting file was empty. Ignoring.\n")
			return nil
		}

		var result map[string]interface{}
		if err := set.env.server.Post(&result, "acvp/v1/"+set.name, newContents); err != nil {
			return err
		}

		// In case it's a testSession that was just created, poke any access token
		// into the server's lookup table and the cache.
		if urlInterface, ok := result["url"]; ok {
			if url, ok := urlInterface.(string); ok {
				if tokenInterface, ok := result["accessToken"]; ok {
					if token, ok := tokenInterface.(string); ok {
						for strings.HasPrefix(url, "/") {
							url = url[1:]
						}
						set.env.server.PrefixTokens[url] = token
						if len(set.env.config.SessionTokensCache) > 0 {
							ioutil.WriteFile(filepath.Join(set.env.config.SessionTokensCache, neturl.PathEscape(url))+".token", []byte(token), 0600)
						}
					}
				}
			}
		}

		ret, err := json.MarshalIndent(result, "", "  ")
		if err != nil {
			return err
		}
		set.env.term.Write(ret)
		return nil
	}
}

type ServerObject struct {
	set   *ServerObjectSet
	index uint64
}

func (obj ServerObject) String() (string, error) {
	data := reflect.New(obj.set.resultType).Interface()
	if err := obj.set.env.server.Get(data, "acvp/v1/"+obj.set.name+"/"+strconv.FormatUint(obj.index, 10)); err != nil {
		return "", err
	}
	ret, err := json.MarshalIndent(data, "", "  ")
	return string(ret), err
}

func (obj ServerObject) Index(index string) (Object, error) {
	if obj.set.subObjects == nil {
		return nil, errors.New("cannot index " + obj.set.name + " objects")
	}
	constr, ok := obj.set.subObjects[index]
	if !ok {
		return nil, fmt.Errorf("no such subobject %q", index)
	}
	return constr(obj.set.env, fmt.Sprintf("%s/%d", obj.set.name, obj.index))
}

func (ServerObject) Search(condition acvp.Query) (Object, error) {
	return nil, errors.New("cannot search individual object")
}

func edit(initialContents string) ([]byte, error) {
	tmp, err := ioutil.TempFile("", "acvp*.json")
	if err != nil {
		return nil, err
	}
	path := tmp.Name()
	defer os.Remove(path)

	_, err = io.WriteString(tmp, initialContents)
	tmp.Close()
	if err != nil {
		return nil, err
	}

	editor := os.Getenv("EDITOR")
	if len(editor) == 0 {
		editor = "vim"
	}

	cmd := exec.Command(editor, path)
	cmd.Stdout = os.Stdout
	cmd.Stdin = os.Stdin
	cmd.Stderr = os.Stderr
	if err := cmd.Run(); err != nil {
		return nil, err
	}

	return ioutil.ReadFile(path)
}

func (obj ServerObject) Action(action string, args []string) error {
	switch action {
	default:
		return fmt.Errorf("unknown action %q", action)

	case "edit":
		if len(args) != 0 {
			return fmt.Errorf("found %d arguments but %q takes none", len(args), action)
		}

		contents, err := obj.String()
		if err != nil {
			return err
		}

		newContents, err := edit(contents)
		if err != nil {
			return err
		}

		if trimmed := strings.TrimSpace(string(newContents)); len(trimmed) == 0 || trimmed == strings.TrimSpace(contents) {
			io.WriteString(obj.set.env.term, "Resulting file was equal or empty. Not updating.\n")
			return nil
		}

		var status acvp.RequestStatus
		if err := obj.set.env.server.Put(&status, "acvp/v1/"+obj.set.name+"/"+strconv.FormatUint(obj.index, 10), newContents); err != nil {
			return err
		}

		fmt.Fprintf(obj.set.env.term, "%#v\n", status)
		return nil

	case "delete":
		if len(args) != 0 {
			return fmt.Errorf("found %d arguments but %q takes none", len(args), action)
		}
		return obj.set.env.server.Delete("acvp/v1/" + obj.set.name + "/" + strconv.FormatUint(obj.index, 10))
	}
}

type Search struct {
	ServerObjectSet
	query acvp.Query
}

func (search Search) String() (string, error) {
	data := reflect.New(reflect.SliceOf(search.resultType)).Interface()
	fmt.Printf("Searching for %#v\n", search.query)
	if err := search.env.server.GetPaged(data, "acvp/v1/"+search.name, search.query); err != nil {
		return "", err
	}
	ret, err := json.MarshalIndent(data, "", "  ")
	return string(ret), err
}

func (search Search) Index(_ string) (Object, error) {
	return nil, errors.New("indexing of search results not supported")
}

func (search Search) Search(condition acvp.Query) (Object, error) {
	search.query = append(search.query, condition...)
	return search, nil
}

func (Search) Action(_ string, _ []string) error {
	return errors.New("no actions supported on search objects")
}

type Algorithms struct {
	ServerObjectSet
}

func (algos Algorithms) String() (string, error) {
	var result struct {
		Algorithms []map[string]interface{} `json:"algorithms"`
	}
	if err := algos.env.server.Get(&result, "acvp/v1/algorithms"); err != nil {
		return "", err
	}
	ret, err := json.MarshalIndent(result.Algorithms, "", "  ")
	return string(ret), err
}

type Env struct {
	line      string
	variables map[string]Object
	server    *acvp.Server
	term      *terminal.Terminal
	config    Config
}

func (e *Env) bytes(node *node32) []byte {
	return []byte(e.line[node.begin:node.end])
}

func (e *Env) contents(node *node32) string {
	return e.line[node.begin:node.end]
}

type stringLiteral struct {
	env      *Env
	contents string
}

func (s stringLiteral) String() (string, error) {
	return s.contents, nil
}

func (stringLiteral) Index(_ string) (Object, error) {
	return nil, errors.New("cannot index strings")
}

func (stringLiteral) Search(_ acvp.Query) (Object, error) {
	return nil, errors.New("cannot search strings")
}

func (s stringLiteral) Action(action string, args []string) error {
	switch action {
	default:
		return fmt.Errorf("action %q not supported on string literals", action)

	case "GET":
		if len(args) != 0 {
			return fmt.Errorf("found %d arguments but %q takes none", len(args), action)
		}

		var results map[string]interface{}
		if err := s.env.server.Get(&results, s.contents); err != nil {
			return err
		}
		ret, err := json.MarshalIndent(results, "", "  ")
		if err != nil {
			return err
		}
		s.env.term.Write(ret)
		return nil
	}
}

type results struct {
	env    *Env
	prefix string
}

func (r results) String() (string, error) {
	var results map[string]interface{}
	if err := r.env.server.Get(&results, "acvp/v1/"+r.prefix+"/results"); err != nil {
		return "", err
	}
	ret, err := json.MarshalIndent(results, "", "  ")
	return string(ret), err
}

func (results) Index(_ string) (Object, error) {
	return nil, errors.New("cannot index results objects")
}

func (results) Search(_ acvp.Query) (Object, error) {
	return nil, errors.New("cannot search results objects")
}

func (results) Action(_ string, _ []string) error {
	return errors.New("no actions supported on results objects")
}

func (e *Env) parseStringLiteral(node *node32) string {
	assertNodeType(node, ruleStringLiteral)
	in := e.bytes(node)
	var buf bytes.Buffer
	for i := 1; i < len(in)-1; i++ {
		if in[i] == '\\' {
			switch in[i+1] {
			case '\\':
				buf.WriteByte('\\')
			case 'n':
				buf.WriteByte('\n')
			case '"':
				buf.WriteByte('"')
			default:
				panic("unknown escape")
			}
			i++
			continue
		}
		buf.WriteByte(in[i])
	}

	return buf.String()
}

func (e *Env) evalExpression(node *node32) (obj Object, err error) {
	switch node.pegRule {
	case ruleStringLiteral:
		return stringLiteral{e, e.parseStringLiteral(node)}, nil

	case ruleVariable:
		varName := e.contents(node)
		obj, ok := e.variables[varName]
		if !ok {
			return nil, fmt.Errorf("unknown variable %q", varName)
		}
		return obj, nil

	case ruleIndexing:
		node = node.up
		assertNodeType(node, ruleVariable)
		varName := e.contents(node)
		obj, ok := e.variables[varName]
		if !ok {
			return nil, fmt.Errorf("unknown variable %q", varName)
		}

		node = node.next
		for node != nil {
			assertNodeType(node, ruleIndex)
			indexStr := e.contents(node)
			if obj, err = obj.Index(indexStr); err != nil {
				return nil, err
			}
			node = node.next
		}

		return obj, nil

	case ruleSearch:
		node = node.up
		assertNodeType(node, ruleVariable)
		varName := e.contents(node)
		obj, ok := e.variables[varName]
		if !ok {
			return nil, fmt.Errorf("unknown variable %q", varName)
		}

		node = skipWS(node.next)
		assertNodeType(node, ruleQuery)
		node = node.up

		var query acvp.Query
		for node != nil {
			assertNodeType(node, ruleConjunctions)
			query = append(query, e.parseConjunction(node.up))
			node = skipWS(node.next)
		}

		if len(query) == 0 {
			return nil, errors.New("cannot have empty query")
		}

		return obj.Search(query)
	}

	panic("unhandled")
}

func (e *Env) evalAction(node *node32) error {
	assertNodeType(node, ruleExpression)
	obj, err := e.evalExpression(node.up)
	if err != nil {
		return err
	}

	node = node.next
	assertNodeType(node, ruleCommand)
	node = node.up
	assertNodeType(node, ruleFunction)
	function := e.contents(node)
	node = node.next

	var args []string
	for node != nil {
		assertNodeType(node, ruleArgs)
		node = node.up
		args = append(args, e.parseStringLiteral(node))

		node = skipWS(node.next)
	}

	return obj.Action(function, args)
}

func (e *Env) parseConjunction(node *node32) (ret acvp.Conjunction) {
	for node != nil {
		assertNodeType(node, ruleConjunction)
		ret = append(ret, e.parseCondition(node.up))

		node = skipWS(node.next)
		if node != nil {
			assertNodeType(node, ruleConjunctions)
			node = node.up
		}
	}
	return ret
}

func (e *Env) parseCondition(node *node32) (ret acvp.Condition) {
	assertNodeType(node, ruleField)
	ret.Param = e.contents(node)
	node = skipWS(node.next)

	assertNodeType(node, ruleRelation)
	switch e.contents(node) {
	case "==":
		ret.Relation = acvp.Equals
	case "!=":
		ret.Relation = acvp.NotEquals
	case "contains":
		ret.Relation = acvp.Contains
	case "startsWith":
		ret.Relation = acvp.StartsWith
	case "endsWith":
		ret.Relation = acvp.EndsWith
	default:
		panic("relation not handled: " + e.contents(node))
	}
	node = skipWS(node.next)

	ret.Value = e.parseStringLiteral(node)

	return ret
}

func runInteractive(server *acvp.Server, config Config) {
	oldState, err := terminal.MakeRaw(0)
	if err != nil {
		panic(err)
	}
	defer terminal.Restore(0, oldState)
	term := terminal.NewTerminal(os.Stdin, "> ")

	resizeChan := make(chan os.Signal, 1)
	go func() {
		for _ = range resizeChan {
			updateTerminalSize(term)
		}
	}()
	signal.Notify(resizeChan, syscall.SIGWINCH)

	env := &Env{variables: make(map[string]Object), server: server, term: term, config: config}
	env.variables["requests"] = ServerObjectSet{
		env:          env,
		name:         "requests",
		resultType:   reflect.TypeOf(&acvp.RequestStatus{}),
		canEnumerate: true,
	}
	env.variables["vendors"] = ServerObjectSet{
		env:  env,
		name: "vendors",
		searchKeys: map[string][]acvp.Relation{
			"name":        []acvp.Relation{acvp.Equals, acvp.StartsWith, acvp.EndsWith, acvp.Contains},
			"website":     []acvp.Relation{acvp.Equals, acvp.StartsWith, acvp.EndsWith, acvp.Contains},
			"email":       []acvp.Relation{acvp.Equals, acvp.StartsWith, acvp.EndsWith, acvp.Contains},
			"phoneNumber": []acvp.Relation{acvp.Equals, acvp.StartsWith, acvp.EndsWith, acvp.Contains},
		},
		subObjects: map[string]func(*Env, string) (Object, error){
			"contacts": func(env *Env, prefix string) (Object, error) {
				return ServerObjectSet{
					env:          env,
					name:         prefix + "/contacts",
					resultType:   reflect.TypeOf(&acvp.Person{}),
					canEnumerate: true,
				}, nil
			},
			"addresses": func(env *Env, prefix string) (Object, error) {
				return ServerObjectSet{
					env:          env,
					name:         prefix + "/addresses",
					resultType:   reflect.TypeOf(&acvp.Address{}),
					canEnumerate: true,
				}, nil
			},
		},
		resultType: reflect.TypeOf(&acvp.Vendor{}),
	}
	env.variables["persons"] = ServerObjectSet{
		env:  env,
		name: "persons",
		searchKeys: map[string][]acvp.Relation{
			"fullName":    []acvp.Relation{acvp.Equals, acvp.StartsWith, acvp.EndsWith, acvp.Contains},
			"email":       []acvp.Relation{acvp.Equals, acvp.StartsWith, acvp.EndsWith, acvp.Contains},
			"phoneNumber": []acvp.Relation{acvp.Equals, acvp.StartsWith, acvp.EndsWith, acvp.Contains},
			"vendorId":    []acvp.Relation{acvp.Equals, acvp.NotEquals, acvp.LessThan, acvp.LessThanEqual, acvp.GreaterThan, acvp.GreaterThanEqual},
		},
		resultType: reflect.TypeOf(&acvp.Person{}),
	}
	env.variables["modules"] = ServerObjectSet{
		env:  env,
		name: "modules",
		searchKeys: map[string][]acvp.Relation{
			"name":        []acvp.Relation{acvp.Equals, acvp.StartsWith, acvp.EndsWith, acvp.Contains},
			"version":     []acvp.Relation{acvp.Equals, acvp.StartsWith, acvp.EndsWith, acvp.Contains},
			"website":     []acvp.Relation{acvp.Equals, acvp.StartsWith, acvp.EndsWith, acvp.Contains},
			"description": []acvp.Relation{acvp.Equals, acvp.StartsWith, acvp.EndsWith, acvp.Contains},
			"type":        []acvp.Relation{acvp.Equals, acvp.NotEquals},
			"vendorId":    []acvp.Relation{acvp.Equals, acvp.NotEquals, acvp.LessThan, acvp.LessThanEqual, acvp.GreaterThan, acvp.GreaterThanEqual},
		},
		resultType: reflect.TypeOf(&acvp.Module{}),
	}
	env.variables["oes"] = ServerObjectSet{
		env:  env,
		name: "oes",
		searchKeys: map[string][]acvp.Relation{
			"name": []acvp.Relation{acvp.Equals, acvp.StartsWith, acvp.EndsWith, acvp.Contains},
		},
		resultType: reflect.TypeOf(&acvp.OperationalEnvironment{}),
	}
	env.variables["deps"] = ServerObjectSet{
		env:  env,
		name: "dependencies",
		searchKeys: map[string][]acvp.Relation{
			"name":        []acvp.Relation{acvp.Equals, acvp.StartsWith, acvp.EndsWith, acvp.Contains},
			"type":        []acvp.Relation{acvp.Equals, acvp.StartsWith, acvp.EndsWith, acvp.Contains},
			"description": []acvp.Relation{acvp.Equals, acvp.StartsWith, acvp.EndsWith, acvp.Contains},
		},
		resultType: reflect.TypeOf(&acvp.Dependency{}),
	}
	env.variables["algos"] = Algorithms{
		ServerObjectSet{
			env:          env,
			name:         "algorithms",
			resultType:   reflect.TypeOf(&acvp.Algorithm{}),
			canEnumerate: true,
		},
	}
	env.variables["sessions"] = ServerObjectSet{
		env:          env,
		name:         "testSessions",
		resultType:   reflect.TypeOf(&acvp.TestSession{}),
		canEnumerate: true,
		subObjects: map[string]func(env *Env, prefix string) (Object, error){
			"results": func(env *Env, prefix string) (Object, error) {
				return results{env: env, prefix: prefix}, nil
			},
		},
	}

	for {
		if env.line, err = term.ReadLine(); err != nil {
			return
		}
		if len(env.line) == 0 {
			continue
		}

		stmt := Statement{Buffer: env.line, Pretty: true}
		stmt.Init()
		if err := stmt.Parse(); err != nil {
			io.WriteString(term, err.Error())
			continue
		}

		node := skipWS(stmt.AST().up)
		switch node.pegRule {
		case ruleExpression:
			obj, err := env.evalExpression(node.up)
			var repr string
			if err == nil {
				repr, err = obj.String()
			}

			if err != nil {
				fmt.Fprintf(term, "error while evaluating expression: %s\n", err)
			} else {
				io.WriteString(term, repr)
				io.WriteString(term, "\n")
			}

		case ruleAction:
			if err := env.evalAction(node.up); err != nil {
				io.WriteString(term, err.Error())
				io.WriteString(term, "\n")
			}

		default:
			fmt.Fprintf(term, "internal error parsing input.\n")
		}
	}
}
