// Package spamassassin will return results from either a SpamAssassin server or
// Postmark's public API depending on configuration
package spamassassin

import (
	"errors"
	"math"
	"strconv"
	"strings"

	"github.com/axllent/mailpit/internal/spamassassin/postmark"
	"github.com/axllent/mailpit/internal/spamassassin/spamc"
)

var (
	// Service to use, either "<host>:<ip>" for self-hosted SpamAssassin or "postmark"
	service string

	// SpamScore is the score at which a message is determined to be spam
	spamScore = 5.0

	// Timeout in seconds
	timeout = 8
)

// Result is a SpamAssassin result
//
// swagger:model SpamAssassinResponse
type Result struct {
	// Whether the message is spam or not
	IsSpam bool
	// If populated will return an error string
	Error string
	// Total spam score based on triggered rules
	Score float64
	// Spam rules triggered
	Rules []Rule
}

// Rule struct
type Rule struct {
	// Spam rule score
	Score float64
	// SpamAssassin rule name
	Name string
	// SpamAssassin rule description
	Description string
}

// SetService defines which service should be used.
func SetService(s string) {
	switch s {
	case "postmark":
		service = "postmark"
	default:
		service = s
	}
}

// SetTimeout defines the timeout
func SetTimeout(t int) {
	if t > 0 {
		timeout = t
	}
}

// Ping returns whether a service is active or not
func Ping() error {
	if service == "postmark" {
		return nil
	}

	var client *spamc.Client
	if strings.HasPrefix("unix:", service) {
		client = spamc.NewUnix(strings.TrimLeft(service, "unix:"))
	} else {
		client = spamc.NewTCP(service, timeout)
	}

	return client.Ping()
}

// Check will return a Result
func Check(msg []byte) (Result, error) {
	r := Result{Score: 0}

	if service == "" {
		return r, errors.New("no SpamAssassin service defined")
	}

	if service == "postmark" {
		res, err := postmark.Check(msg, timeout)
		if err != nil {
			r.Error = err.Error()
			return r, nil
		}
		resFloat, err := strconv.ParseFloat(res.Score, 32)
		if err == nil {
			r.Score = round1dm(resFloat)
			r.IsSpam = resFloat >= spamScore
		}
		r.Error = res.Message
		for _, pr := range res.Rules {
			rule := Rule{}
			value, err := strconv.ParseFloat(pr.Score, 32)
			if err == nil {
				rule.Score = round1dm(value)
			}
			rule.Name = pr.Name
			rule.Description = pr.Description
			r.Rules = append(r.Rules, rule)
		}
	} else {
		var client *spamc.Client
		if strings.HasPrefix("unix:", service) {
			client = spamc.NewUnix(strings.TrimLeft(service, "unix:"))
		} else {
			client = spamc.NewTCP(service, timeout)
		}

		res, err := client.Report(msg)
		if err != nil {
			r.Error = err.Error()
			return r, nil
		}
		r.IsSpam = res.Score >= spamScore
		r.Score = round1dm(res.Score)
		r.Rules = []Rule{}
		for _, sr := range res.Rules {
			rule := Rule{}
			value, err := strconv.ParseFloat(sr.Points, 32)
			if err == nil {
				rule.Score = round1dm(value)
			}
			rule.Name = sr.Name
			rule.Description = sr.Description
			r.Rules = append(r.Rules, rule)
		}
	}

	return r, nil
}

// Round to one decimal place
func round1dm(n float64) float64 {
	return math.Floor(n*10) / 10
}