// Package filter evaluates deterministic, local-only Usenet filters.
package filter

import (
	"fmt"
	"path"
	"regexp"
	"strings"
	"time"
)

type Field string

const (
	FieldAny        Field = "any"
	FieldFrom       Field = "from"
	FieldSubject    Field = "subject"
	FieldNewsgroups Field = "newsgroups"
	FieldMessageID  Field = "message-id"
	FieldReferences Field = "references"
	FieldDate       Field = "date"
	FieldBody       Field = "body"
	FieldHeader     Field = "header"
	FieldVFaceHash  Field = "vface-hash"
	FieldRead       Field = "read"
)

type Operator string

const (
	OperatorContains Operator = "contains"
	OperatorExact    Operator = "exact"
	OperatorRegexp   Operator = "regexp"
	OperatorGlob     Operator = "glob"
)

type Action string

const (
	ActionKeep       Action = "keep"
	ActionHide       Action = "hide"
	ActionMarkRead   Action = "mark-read"
	ActionHighlight  Action = "highlight"
	ActionTag        Action = "tag"
	ActionMuteThread Action = "mute-thread"
)

type Rule struct {
	ID       string   `json:"id"`
	Enabled  bool     `json:"enabled"`
	Field    Field    `json:"field"`
	Header   string   `json:"header,omitempty"`
	Operator Operator `json:"operator"`
	Pattern  string   `json:"pattern"`
	Action   Action   `json:"action"`
	Tag      string   `json:"tag,omitempty"`
	Reason   string   `json:"reason,omitempty"`
}

type Article struct {
	From       string
	Subject    string
	Newsgroups string
	MessageID  string
	References string
	Date       string
	Body       string
	Read       bool
	Headers    map[string]string
}

type Result struct {
	Hidden       bool
	MarkRead     bool
	Highlight    bool
	MuteThread   bool
	Tags         []string
	MatchedRules []string
}

type InvalidRuleError struct{ Message string }

func (e *InvalidRuleError) Error() string { return e.Message }

func Evaluate(article Article, rules []Rule) (Result, error) {
	var result Result
	for _, rule := range rules {
		if !rule.Enabled {
			continue
		}
		if err := validateRule(rule); err != nil {
			return Result{}, err
		}
		values := fieldValues(article, rule)
		matched, err := match(values, rule.Operator, rule.Pattern)
		if err != nil {
			return Result{}, fmt.Errorf("rule %q: %w", rule.ID, err)
		}
		if !matched {
			continue
		}
		result.MatchedRules = append(result.MatchedRules, rule.ID)
		switch rule.Action {
		case ActionHide:
			result.Hidden = true
		case ActionMarkRead:
			result.MarkRead = true
		case ActionHighlight:
			result.Highlight = true
		case ActionTag:
			if rule.Tag != "" && !contains(result.Tags, rule.Tag) {
				result.Tags = append(result.Tags, rule.Tag)
			}
		case ActionMuteThread:
			result.MuteThread = true
		case ActionKeep:
		default:
			return Result{}, &InvalidRuleError{Message: fmt.Sprintf("rule %q has unsupported action %q", rule.ID, rule.Action)}
		}
	}
	return result, nil
}

func validateRule(rule Rule) error {
	if strings.TrimSpace(rule.Pattern) == "" {
		return &InvalidRuleError{Message: fmt.Sprintf("rule %q has an empty pattern", rule.ID)}
	}
	switch rule.Field {
	case FieldAny, FieldFrom, FieldSubject, FieldNewsgroups, FieldMessageID,
		FieldReferences, FieldDate, FieldBody, FieldHeader, FieldVFaceHash, FieldRead:
	default:
		return &InvalidRuleError{Message: fmt.Sprintf("rule %q has unsupported field %q", rule.ID, rule.Field)}
	}
	if rule.Field == FieldHeader && strings.TrimSpace(rule.Header) == "" {
		return &InvalidRuleError{Message: fmt.Sprintf("rule %q needs a header name", rule.ID)}
	}
	switch rule.Operator {
	case OperatorContains, OperatorExact, OperatorRegexp, OperatorGlob:
	default:
		return &InvalidRuleError{Message: fmt.Sprintf("rule %q has unsupported operator %q", rule.ID, rule.Operator)}
	}
	switch rule.Action {
	case ActionKeep, ActionHide, ActionMarkRead, ActionHighlight, ActionTag, ActionMuteThread:
	default:
		return &InvalidRuleError{Message: fmt.Sprintf("rule %q has unsupported action %q", rule.ID, rule.Action)}
	}
	return nil
}

func fieldValues(article Article, rule Rule) []string {
	if rule.Field == FieldAny {
		return []string{article.From, article.Subject, article.Newsgroups, article.MessageID, article.References, article.Date, article.Body, vfaceValue(article.Headers)}
	}
	if rule.Field == FieldHeader {
		for key, value := range article.Headers {
			if strings.EqualFold(key, rule.Header) {
				return []string{value}
			}
		}
		return nil
	}
	switch rule.Field {
	case FieldFrom:
		return []string{article.From}
	case FieldSubject:
		return []string{article.Subject}
	case FieldNewsgroups:
		return []string{article.Newsgroups}
	case FieldMessageID:
		return []string{article.MessageID}
	case FieldReferences:
		return []string{article.References}
	case FieldDate:
		return []string{article.Date}
	case FieldBody:
		return []string{article.Body}
	case FieldVFaceHash:
		return []string{vfaceValue(article.Headers)}
	case FieldRead:
		return []string{fmt.Sprintf("%t", article.Read)}
	default:
		return nil
	}
}

func vfaceValue(headers map[string]string) string {
	for _, key := range []string{"Identity-Hash", "X-VFace-Hash", "X-VFace-PNG-SHA256"} {
		for header, value := range headers {
			if strings.EqualFold(header, key) {
				return value
			}
		}
	}
	return ""
}

func match(values []string, operator Operator, pattern string) (bool, error) {
	for _, value := range values {
		switch operator {
		case OperatorContains:
			if strings.Contains(strings.ToLower(value), strings.ToLower(pattern)) {
				return true, nil
			}
		case OperatorExact:
			if strings.EqualFold(strings.TrimSpace(value), strings.TrimSpace(pattern)) {
				return true, nil
			}
		case OperatorRegexp:
			re, err := regexp.Compile(pattern)
			if err != nil {
				return false, err
			}
			if re.MatchString(value) {
				return true, nil
			}
		case OperatorGlob:
			matched, err := path.Match(strings.ToLower(pattern), strings.ToLower(value))
			if err != nil {
				return false, err
			}
			if matched {
				return true, nil
			}
		}
	}
	return false, nil
}

func contains(values []string, wanted string) bool {
	for _, value := range values {
		if value == wanted {
			return true
		}
	}
	return false
}

// ParseDate is kept here so callers can validate date filters without adding
// parser logic to the UI. It accepts the common RFC 5322 form and RFC3339.
func ParseDate(value string) (time.Time, error) {
	if parsed, err := time.Parse(time.RFC1123Z, value); err == nil {
		return parsed, nil
	}
	return time.Parse(time.RFC3339, value)
}
