package controllers

import (
	"bytes"
	"context"
	"encoding/json"
	"io"
	"io/ioutil"
	"net/http"
	"strings"
	"strconv"

	"github.com/golang/protobuf/jsonpb"
	"gitlab.com/gitlab-org/labkit/log"
	spamcheck "gitlab.com/gitlab-org/spamcheck/api/v1"
	timestamppb "google.golang.org/protobuf/types/known/timestamppb"
	config "gitlab.com/gitlab-org/spamcheck/app/config"
)

const (
	grpcServerPort   = "5380"
	externalRestPort = "5381"
)

type httpClient interface {
	Post(url, contentType string, body io.Reader) (resp *http.Response, err error)
}

// Client is a publicly-accessible abstraction for http.Client so it can be mocked
var Client httpClient

// SpamCheckController Is the core of the business logic that controls how objects are checked for spam
type SpamCheckController interface {
	spamcheck.SpamcheckServiceServer
}

type spamCheckController struct {
	encoder *jsonpb.Marshaler
	*jsonpb.Marshaler
	cfg *config.Config

	//Needed for GRPC V2, not ours.
	spamcheck.UnimplementedSpamcheckServiceServer
}

// JSONPayload define structs for our issue JSON payload
type jsonPayload struct {
	Entity entity `json:"entity"`
}

type entity struct {
	ID            uint                   `json:"id"`
	User          user                   `json:"user"`
	Title         string                 `json:"title"`
	Description   string                 `json:"description"`
	CreatedAt     *timestamppb.Timestamp `json:"created_at"`
	UpdatedAt     *timestamppb.Timestamp `json:"updated_at"`
	Action        spamcheck.Action       `json:"action"`
	UserInProject bool                   `json:"user_in_project"`
}

type user struct {
	Emails    []*spamcheck.User_Email `json:"emails"`
	Org       string                  `json:"org"`
	Username  string                  `json:"username"`
	CreatedAt *timestamppb.Timestamp  `json:"created_at"`
}

type userEmail struct {
	Email    string `json:"email"`
	Verified bool   `json:"verified"`
}


// CreateSpamCheckController is the "constructor" SpamCheckController
func CreateSpamCheckController(cfg *config.Config) SpamCheckController {
	Client = &http.Client{}

	encoder := &jsonpb.Marshaler{OrigName: true}
	log.Info("Inspector URL: ", cfg.Inspector.InspectorURL)
	return &spamCheckController{
		encoder:      encoder,
		cfg: 		  cfg,
	}
}

// CreateJSONPayload creates our JSON payload struct
func (s *spamCheckController) createJSONPayload(issue *spamcheck.Issue) *jsonPayload {
	currentUser := issue.GetUser()
	user := user{
		Emails:    currentUser.GetEmails(),
		Org:       currentUser.GetOrg(),
		Username:  currentUser.GetUsername(),
		CreatedAt: currentUser.GetCreatedAt(),
	}

	entityPayload := entity{
		ID:            1,
		User:          user,
		Title:         issue.GetTitle(),
		Description:   issue.GetDescription(),
		CreatedAt:     issue.GetCreatedAt(),
		UpdatedAt:     issue.GetUpdatedAt(),
		Action:        issue.GetAction(),
		UserInProject: issue.GetUserInProject(),
	}

	jsonPayload := jsonPayload{
		Entity: entityPayload,
	}
	return &jsonPayload
}

// PostJSONPayload posts issue JSON to Inspector
func (s *spamCheckController) postJSONPayload(jp *jsonPayload, inspectorURL string) []byte {
	// serializing the struct into JSON
	jsonValue, err := json.Marshal(jp)
	if err != nil {
		log.WithFields(log.Fields{
			"metric": "spamcheck_inspector_errors",
		}).WithError(err).Error("Error marshalling issue to JSON")
		return []byte{}
	}

	resp, err := Client.Post(s.cfg.Inspector.InspectorURL, "application/json", bytes.NewBuffer(jsonValue))
	if err != nil {
		log.WithFields(log.Fields{
			"metric": "spamcheck_inspector_errors",
		}).WithError(err).Error("Error querying Inspector")
		return []byte{}
	}
	defer resp.Body.Close()

	body, _ := ioutil.ReadAll(resp.Body)

	return body
}

func (s *spamCheckController) IsProjectAllowed(issue *spamcheck.Issue) bool {
	var result bool

	currentProject := issue.GetProject()

	// checks allowlist first by default
	if len(s.cfg.Filter.AllowList) != 0 {
		if val, exists := s.cfg.Filter.AllowList[strconv.Itoa(int(currentProject.GetProjectId()))]; exists {
			// explicitly allowed project
			log.Info("Project ", val, " with id ", currentProject.GetProjectId(), " allowed for spamcheck")
			result = true
		}	else {
			// id doesn't match any key in allowlist
			log.Info("Project ", currentProject.GetProjectPath(), " with id ", currentProject.GetProjectId(), " not explicitly allowed in allowlist")
			result = false
		}
	} else if len(s.cfg.Filter.DenyList) != 0 {
		if val, exists := s.cfg.Filter.DenyList[strconv.Itoa(int(currentProject.GetProjectId()))]; exists {
			// explicitly denied project
			log.Info("Project ", val, " with id ", currentProject.GetProjectId(), " denied for spamcheck")
			result = false
		} else {
			// id doesn't match any key in denylist
			log.Info("Project ", currentProject.GetProjectPath(), " with id ", currentProject.GetProjectId(), " allowed for spamcheck")
			result = true
		}
	} else {
		// if both lists are empty, allow everything
		log.Info("Project ", currentProject.GetProjectPath(), " with id ", currentProject.GetProjectId(), " allowed for spamcheck")
		result = true
	}

	return result
}

// CheckForSpamIssue does the actual checking for spam
func (s *spamCheckController) CheckForSpamIssue(ctx context.Context, issue *spamcheck.Issue) (*spamcheck.SpamVerdict, error) {
	var result spamcheck.SpamVerdict

	attribs := make(map[string]string)
	attribs["monitorMode"] = s.cfg.ExtraAttributes.MonitorMode

	isAllowedProject := s.IsProjectAllowed(issue)
	if isAllowedProject == false {
		result = spamcheck.SpamVerdict{
			Verdict: spamcheck.SpamVerdict_NOOP,
			ExtraAttributes: attribs,
		}
		log.WithFields(log.Fields{
			"metric":       "spamcheck_verdicts",
			"monitor_mode": s.cfg.ExtraAttributes.MonitorMode,
			"verdict":      result.GetVerdict().String(),
		}).Info("Verdict for disallowed project")

		return &result, nil
	}

	jsonPayload := s.createJSONPayload(issue)
	body := s.postJSONPayload(jsonPayload, s.cfg.Inspector.InspectorURL)

	// deserialize response into map
	verdictMap := map[string]interface{}{}
	if err := json.Unmarshal([]byte(body), &verdictMap); err != nil {
		log.WithFields(log.Fields{
			"metric": "spamcheck_inspector_errors",
		}).WithError(err).Error("Unable to unmarshall response from Inspector")

		// Default to NOOP
		result = spamcheck.SpamVerdict{
			Verdict: spamcheck.SpamVerdict_NOOP,
			Error:   err.Error(),
			ExtraAttributes: attribs,
		}
		log.WithFields(log.Fields{
			"metric":       "spamcheck_verdicts",
			"monitor_mode": s.cfg.ExtraAttributes.MonitorMode,
			"verdict":      result.GetVerdict().String(),
		}).Info("Verdict for failed Inspector call")

		return &result, nil
	}
	// cast values from interface to float
	hamVerdict := verdictMap["1"].(map[string]interface{})["ham"].(float64)
	spamVerdict := verdictMap["1"].(map[string]interface{})["spam"].(float64)

	log.WithFields(log.Fields{
		"ham":    hamVerdict,
		"spam":   spamVerdict,
		"metric": "spamcheck_inspector_verdicts",
	}).Info("Inspector verdict received")

	isEmailAllowlisted := s.isEmailAllowlisted(issue.GetUser())
	if isEmailAllowlisted {
		hamVerdict = 1.0
		spamVerdict = 0.0
	}

	if spamVerdict >= 0.9 {
		result = spamcheck.SpamVerdict{
			Verdict: spamcheck.SpamVerdict_BLOCK,
			ExtraAttributes: attribs,
		}
	} else if spamVerdict > 0.5 && spamVerdict < 0.9 {
		result = spamcheck.SpamVerdict{
			Verdict: spamcheck.SpamVerdict_DISALLOW,
			ExtraAttributes: attribs,
		}
	} else if spamVerdict >= 0.4 && spamVerdict <= 0.5 {
		result = spamcheck.SpamVerdict{
			Verdict: spamcheck.SpamVerdict_CONDITIONAL_ALLOW,
			ExtraAttributes: attribs,
		}
	} else {
		result = spamcheck.SpamVerdict{
			Verdict: spamcheck.SpamVerdict_ALLOW,
			ExtraAttributes: attribs,
		}
	}

	log.WithFields(log.Fields{
		"email_allowlisted": isEmailAllowlisted,
		"metric":            "spamcheck_verdicts",
		"monitor_mode":      s.cfg.ExtraAttributes.MonitorMode,
		"verdict":           result.GetVerdict().String(),
	}).Info("Verdict calculated")

	return &result, nil
}

func (s *spamCheckController) isEmailAllowlisted(user *spamcheck.User) bool {
	for _, email := range user.Emails {
		if email.Verified && strings.HasSuffix(email.Email, "@gitlab.com") {
			return true
		}
	}
	return false
}
