package v1

import (
	"crypto/sha512"
	"fmt"
	"net/http"
	"strconv"
	"strings"
	"time"

	"github.com/crowdsecurity/crowdsec/pkg/database/ent"
	"github.com/crowdsecurity/crowdsec/pkg/models"
	"github.com/gin-gonic/gin"
	log "github.com/sirupsen/logrus"
)

func FormatDecisions(decisions []*ent.Decision) ([]*models.Decision, error) {
	var results []*models.Decision
	for _, dbDecision := range decisions {
		duration := dbDecision.Until.Sub(time.Now()).String()
		decision := models.Decision{
			ID:       int64(dbDecision.ID),
			Duration: &duration,
			Scenario: &dbDecision.Scenario,
			Scope:    &dbDecision.Scope,
			Value:    &dbDecision.Value,
			Type:     &dbDecision.Type,
			Origin:   &dbDecision.Origin,
		}
		results = append(results, &decision)
	}
	return results, nil
}

func (c *Controller) GetDecision(gctx *gin.Context) {
	var err error
	var results []*models.Decision
	var data []*ent.Decision

	data, err = c.DBClient.QueryDecisionWithFilter(gctx.Request.URL.Query())
	if err != nil {
		c.HandleDBErrors(gctx, err)
		return
	}

	results, err = FormatDecisions(data)
	if err != nil {
		gctx.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
		return
	}
	/*let's follow a naive logic : when a bouncer queries /decisions, if the answer is empty, we assume there is no decision for this ip/user/...,
	but if it's non-empty, it means that there is one or more decisions for this target*/
	if len(results) > 0 {
		PrometheusBouncersHasNonEmptyDecision(gctx)
	} else {
		PrometheusBouncersHasEmptyDecision(gctx)
	}

	if gctx.Request.Method == "HEAD" {
		gctx.String(http.StatusOK, "")
		return
	}
	gctx.JSON(http.StatusOK, results)
	return
}

func (c *Controller) DeleteDecisionById(gctx *gin.Context) {
	var err error

	decisionIDStr := gctx.Param("decision_id")
	decisionID, err := strconv.Atoi(decisionIDStr)
	if err != nil {
		gctx.JSON(http.StatusBadRequest, gin.H{"message": "decision_id must be valid integer"})
		return
	}
	err = c.DBClient.SoftDeleteDecisionByID(decisionID)
	if err != nil {
		c.HandleDBErrors(gctx, err)
		return
	}

	deleteDecisionResp := models.DeleteDecisionResponse{
		NbDeleted: "1",
	}

	gctx.JSON(http.StatusOK, deleteDecisionResp)
	return
}

func (c *Controller) DeleteDecisions(gctx *gin.Context) {
	var err error

	nbDeleted, err := c.DBClient.SoftDeleteDecisionsWithFilter(gctx.Request.URL.Query())
	if err != nil {
		c.HandleDBErrors(gctx, err)
		return
	}
	deleteDecisionResp := models.DeleteDecisionResponse{
		NbDeleted: nbDeleted,
	}

	gctx.JSON(http.StatusOK, deleteDecisionResp)
	return
}

func (c *Controller) StreamDecision(gctx *gin.Context) {
	var data []*ent.Decision
	ret := make(map[string][]*models.Decision, 0)
	ret["new"] = []*models.Decision{}
	ret["deleted"] = []*models.Decision{}

	val := gctx.Request.Header.Get(c.APIKeyHeader)
	hashedKey := sha512.New()
	hashedKey.Write([]byte(val))
	hashStr := fmt.Sprintf("%x", hashedKey.Sum(nil))
	bouncerInfo, err := c.DBClient.SelectBouncer(hashStr)
	if err != nil {
		if _, ok := err.(*ent.NotFoundError); ok {
			gctx.JSON(http.StatusForbidden, gin.H{"message": err.Error()})
		} else {
			gctx.JSON(http.StatusUnauthorized, gin.H{"message": "not allowed"})
		}
		return
	}

	if bouncerInfo == nil {
		gctx.JSON(http.StatusUnauthorized, gin.H{"message": "not allowed"})
		return
	}

	filters := make(map[string][]string)
	filters["scope"] = []string{"ip", "range"}
	if val, ok := gctx.Request.URL.Query()["scopes"]; ok {
		filters["scope"] = strings.Split(val[0], ",")
	}

	// if the blocker just start, return all decisions
	if val, ok := gctx.Request.URL.Query()["startup"]; ok {
		if val[0] == "true" {
			data, err := c.DBClient.QueryAllDecisionsWithFilters(filters)
			if err != nil {
				log.Errorf("failed querying decisions: %v", err)
				gctx.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
				return
			}
			ret["new"], err = FormatDecisions(data)
			if err != nil {
				log.Errorf("unable to format expired decision for '%s' : %v", bouncerInfo.Name, err)
				gctx.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
				return
			}

			// getting expired decisions
			data, err = c.DBClient.QueryExpiredDecisionsWithFilters(filters)
			if err != nil {
				log.Errorf("unable to query expired decision for '%s' : %v", bouncerInfo.Name, err)
				gctx.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
				return
			}
			ret["deleted"], err = FormatDecisions(data)
			if err != nil {
				log.Errorf("unable to format expired decision for '%s' : %v", bouncerInfo.Name, err)
				gctx.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
				return
			}

			if err := c.DBClient.UpdateBouncerLastPull(time.Now(), bouncerInfo.ID); err != nil {
				log.Errorf("unable to update bouncer '%s' pull: %v", bouncerInfo.Name, err)
				gctx.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
				return
			}
			if gctx.Request.Method == "HEAD" {
				gctx.String(http.StatusOK, "")
				return
			}
			gctx.JSON(http.StatusOK, ret)
			return
		}
	}

	// getting new decisions
	data, err = c.DBClient.QueryNewDecisionsSinceWithFilters(bouncerInfo.LastPull, filters)
	if err != nil {
		log.Errorf("unable to query new decision for '%s' : %v", bouncerInfo.Name, err)
		gctx.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
		return
	}
	ret["new"], err = FormatDecisions(data)
	if err != nil {
		log.Errorf("unable to format new decision for '%s' : %v", bouncerInfo.Name, err)
		gctx.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
		return
	}

	// getting expired decisions
	data, err = c.DBClient.QueryExpiredDecisionsSinceWithFilters(bouncerInfo.LastPull.Add((-2 * time.Second)), filters) // do we want to give exactly lastPull time ?
	if err != nil {
		log.Errorf("unable to query expired decision for '%s' : %v", bouncerInfo.Name, err)
		gctx.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
		return
	}
	ret["deleted"], err = FormatDecisions(data)
	if err != nil {
		log.Errorf("unable to format expired decision for '%s' : %v", bouncerInfo.Name, err)
		gctx.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
		return
	}

	if err := c.DBClient.UpdateBouncerLastPull(time.Now(), bouncerInfo.ID); err != nil {
		log.Errorf("unable to update bouncer '%s' pull: %v", bouncerInfo.Name, err)
		gctx.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
		return
	}

	gctx.JSON(http.StatusOK, ret)
	return
}
