// Author: Matthew Shiel
// Code adapted from https://github.com/kennysong/goeliza/

package eliza

import (
	"bytes"
	"fmt"
	"html/template"
	"math/rand"
	"regexp"
	"strings"
	"time"
)

// InteractiveBot This is the interface to comunicate with the bot
type InteractiveBot interface {
	ReplyTo(statement string) (string, *Personality)
}

// Chatbot Defines data to be used by a chatbot personality - essentially
// Its personality script and execution context
type Chatbot struct {
	Personality *Personality
	Context     *ChatbotContext
}

// ChatbotContext is the white board structure
type ChatbotContext struct {
	EngineVersion string
	Session       SessionData
}

// ChatbotInteraction defines a individual question/answer interaction with a caller
type ChatbotInteraction struct {
	Time     string `json:"time,omitempty" yaml:"time,omitempty"`
	Question string `json:"question,omitempty" yaml:"question,omitempty"`
	Answer   string `json:"answer,omitempty" yaml:"answer,omitempty"`
}

// SessionData defines information about the current bot and its interaction within a specific session
type SessionData struct {
	SessionID    string               `json:"sessionID" yaml:"sessionID"`
	StartTime    string               `json:"startTime,omitempty" yaml:"startTime,omitempty"`
	User         string               `json:"user,omitempty" yaml:"user,omitempty"`
	Bot          string               `json:"bot,omitempty" yaml:"bot,omitempty"`
	BotVersion   string               `json:"botVersion,omitempty" yaml:"botVersion,omitempty"`
	Conversation []ChatbotInteraction `json:"conversation,omitempty" yaml:"conversation,omitempty"`
}

// NewBotPersonality is a utility method to create a bot instance from a personality and its context
func NewBotPersonality(personality *Personality, context *ChatbotContext) *Chatbot {
	return &Chatbot{personality, context}

}

// Greetings will return a random introductory sentence for ELIZA.
func (p *Chatbot) Greetings() string {
	return p.randChoice(p.Personality.Introductions)
}

// GoodbyeResponse will return a random goodbye sentence for ELIZA.
func (p *Chatbot) GoodbyeResponse() string {
	return p.randChoice(p.Personality.Goodbyes)
}

// ReplyTo will construct a reply for a given statement using ELIZA's rules.
func (p *Chatbot) ReplyTo(statement string) string {
	// First, preprocess the statement for more effective matching
	statement = p.preprocess(statement)

	// Then, we check if this is a quit statement
	if p.IsQuitStatement(statement) {
		return p.GoodbyeResponse()
	}

	// Next, we try to match the statement to a statement that ELIZA can
	// recognize, and construct a pre-determined, appropriate response.
	for _, similarQuestionResponse := range p.Personality.Psychobabble {
		for _, question := range similarQuestionResponse.SimilarQuestions {
			re := regexp.MustCompile(question)
			matches := re.FindStringSubmatch(statement)

			// If the statement matched any recognizable statements.
			if len(matches) > 0 {
				// If we matched a regex group in parentheses, get the first match.
				// The matched regex group will match a "fragment" that will form
				// part of the response, for added realism.
				var fragment string
				if len(matches) > 1 {
					fragment = p.reflect(matches[1])
				}

				// Choose a random appropriate response, and format it with the
				// fragment, if needed.
				response := p.randChoice(similarQuestionResponse.Responses)
				if strings.Contains(response, "%s") {
					response = fmt.Sprintf(response, fragment)
				}
				//			fmt.Printf("For Statement \"%s\" got a hit with pattern \"%s\" Responded With \"%s\"\n", statement, pattern, response)
				return p.replacePlaceHolders(response)
			}
		}
	}

	// If no patterns were matched, return a default response.
	return p.replacePlaceHolders(p.randChoice(p.Personality.DefaultResponses))
}

// IsQuitStatement returns if the statement is a quit statement
func (p *Chatbot) IsQuitStatement(statement string) bool {
	statement = p.preprocess(statement)
	for _, quitResponse := range p.Personality.QuitResponses {
		if statement == quitResponse {
			return true
		}
	}
	return false
}

// preprocess will do some normalization on a statement for better regex matching
func (p *Chatbot) preprocess(statement string) string {
	statement = strings.TrimRight(statement, "\n.!")
	statement = strings.ToLower(statement)
	return statement
}

// reflect flips a few words in an input fragment (such as "I" -> "you").
func (p *Chatbot) reflect(fragment string) string {
	words := strings.Split(fragment, " ")
	for i, word := range words {
		if reflectedWord, ok := p.Personality.ReflectedWords[word]; ok {
			words[i] = reflectedWord
		}
	}
	return strings.Join(words, " ")
}

// randChoice returns a random element in an (string) array.
func (p *Chatbot) randChoice(list []string) string {
	// Added for truly random generation of numbers with seeds
	if len(list) == 0 {
		return ""
	}
	rand.Seed(time.Now().UnixNano())
	randIndex := rand.Intn(len(list))
	return list[randIndex]
}

/**
* Replaces conversation templates (names etc) in reply with
 */
func (p *Chatbot) replacePlaceHolders(answer string) string {
	var tBuffer bytes.Buffer
	t := template.Must(template.New("answer").Parse(answer))
	t.Execute(&tBuffer, p.Context)

	return tBuffer.String()
}