package matcher

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"log/slog"
	"os"
	"regexp"
	"strings"
	"text/template"
	"time"

	"gopkg.in/yaml.v3"
)

var CommonFuncs = template.FuncMap{
	"toUpper": strings.ToUpper,
	"toLower": strings.ToLower,
	"join": func(sep string, s []string) string {
		return strings.Join(s, sep)
	},
	"reMatch": func(pattern, text string) bool {
		re := regexp.MustCompile(pattern)
		return re.MatchString(text)
	},
	"reReplaceAll": func(pattern, repl, text string) string {
		re := regexp.MustCompile(pattern)
		return re.ReplaceAllString(text, repl)
	},
	"reSubmatch": func(pattern, text string) []string {
		re := regexp.MustCompile(pattern)
		return re.FindStringSubmatch(text)
	},
	"stringSlice": func(s ...string) []string {
		return s
	},
	"tryUnmarshalJson": func(j interface{}) interface{} {
		jsonStr, ok := j.(string)
		if !ok {
			return nil
		}

		var jsonVal interface{}
		err := json.Unmarshal([]byte(jsonStr), &jsonVal)
		if err != nil {
			return nil
		}
		return jsonVal
	},
	"isDayTime": func() bool {
		now := time.Now()

		hour := now.UTC().Hour()
		if hour >= 1 && hour <= 15 {
			return true
		}

		return false
	},
	"isNightTime05": func() bool {
		//between utc+8 [12 am ~ 5 am)
		now := time.Now()

		hour := now.UTC().Hour()
		if hour >= 16 && hour < 21 {
			return true
		}
		return false
	},
	"isNightTime59": func() bool {
		//between utc+8 [5 pm ~ 9 am)
		now := time.Now()
		hour := now.UTC().Hour()
		if hour >= 21 || hour < 1 {
			return true
		}
		return false
	},
	"isDayTime16": func() bool {
		now := time.Now()

		hour := now.UTC().Hour()
		if hour >= 1 && hour <= 16 {
			return true
		}

		return false
	},
	"isNightTime012": func() bool {
		//between utc+8 [12 am ~ 12 pm)
		now := time.Now()

		hour := now.UTC().Hour()
		if hour >= 16 || hour < 4 {
			return true
		}

		return false
	},
	"isWeekday": func() bool {
		now := time.Now()
		weekday := now.UTC().Weekday()
		if weekday >= time.Monday && weekday <= time.Friday {
			return true
		}
		return false
	},
}

type Extractor struct {
	rawTemplate string
	Template    *template.Template
}

func (e *Extractor) UnmarshalYAML(unmarshal func(interface{}) error) error {
	var rawTemplate string
	err := unmarshal(&rawTemplate)
	if err != nil {
		return err
	}

	e.rawTemplate = rawTemplate

	tmpl := template.New("").Funcs(CommonFuncs).Option("missingkey=zero")
	tmpl, err = tmpl.Parse(e.rawTemplate)
	if err != nil {
		return fmt.Errorf("parse error in %#v, %w", e.rawTemplate, err)
	}

	e.Template = tmpl

	return err
}

func (e Extractor) MarshalText() (text []byte, err error) {
	return []byte(e.rawTemplate), nil
}

type Extractors map[string]Extractor

type Receiver interface{}

type Receivers []Receiver

var ErrorReceiverType = errors.New("type unmatch, can not transmute receivers")

func (rs *Receivers) TryToIntSlice() ([]int, error) {
	raw := make([]int, 0)
	for _, r := range *rs {
		intR, ok := r.(int)
		if !ok {
			return nil, ErrorReceiverType
		}
		raw = append(raw, intR)
	}
	return raw, nil
}

func (rs *Receivers) TryToStringSlice() ([]string, error) {
	raw := make([]string, 0)
	for _, r := range *rs {
		intR, ok := r.(string)
		if !ok {
			return nil, ErrorReceiverType
		}
		raw = append(raw, intR)
	}
	return raw, nil
}

type ReMatcher struct {
	rawRegexp string
	Regexp    *regexp.Regexp
}

func (rm *ReMatcher) UnmarshalYAML(unmarshal func(interface{}) error) error {
	var rawRegexp string
	err := unmarshal(&rawRegexp)
	if err != nil {
		return err
	}

	rm.rawRegexp = rawRegexp

	re, err := regexp.Compile(rm.rawRegexp)
	if err != nil {
		return err
	}

	rm.Regexp = re

	return err
}

func (rm ReMatcher) MarshalText() (text []byte, err error) {
	return []byte(rm.rawRegexp), nil
}

type Query struct {
	Negative bool
	Label    string
}

func (q *Query) UnmarshalYAML(unmarshal func(interface{}) error) error {
	var rawQuery string
	err := unmarshal(&rawQuery)
	if err != nil {
		return err
	}
	q.Label = strings.TrimPrefix(rawQuery, "!")
	if q.Label == rawQuery {
		q.Negative = false
	} else {
		q.Negative = true
	}
	return err
}

func (q Query) MarshalText() (text []byte, err error) {
	if q.Negative {
		return []byte("!" + q.Label), nil
	} else {
		return []byte(q.Label), nil
	}
}

type Route struct {
	Receiver Receiver            `yaml:"receiver" json:"receiver"`
	Continue bool                `yaml:"continue" json:"continue"`
	Match    map[Query]ReMatcher `yaml:"match" json:"match"`
}

type Routes []Route

type Matcher struct {
	logger     *slog.Logger
	Extractors Extractors `yaml:"extractors" json:"extractors"`
	Routes     Routes     `yaml:"routes" json:"routes"`
}

func NewMatcher(logger *slog.Logger, filepath string) (*Matcher, error) {
	mtcherBytes, err := os.ReadFile(filepath)
	if err != nil {
		return nil, err
	}
	m := &Matcher{logger: logger}
	err = yaml.Unmarshal(mtcherBytes, m)
	if err != nil {
		return nil, err
	}
	err = m.valid()
	if err != nil {
		return nil, err
	}
	return m, nil
}

func (m *Matcher) Match(target interface{}) (Receivers, error) {
	receivers := make([]Receiver, 0)
	receiverSet := make(map[Receiver]struct{}, 0)
	extraced := make(map[string]string)
	for _, r := range m.Routes {
		isMatch := true
		for q, rm := range r.Match {
			l := q.Label
			qVal, ok := extraced[l]
			if !ok {
				e, ok := m.Extractors[l]
				if !ok {
					return nil, fmt.Errorf("label %#v not found in extractors", l)
				}
				var buf bytes.Buffer
				err := e.Template.Funcs(CommonFuncs).Execute(&buf, target)
				if err != nil {
					return nil, err
				}
				qVal = buf.String()
				extraced[l] = qVal
				m.logger.Debug("match", "extractor", e.rawTemplate, "extraced", qVal)
			}
			if rm.Regexp.MatchString(qVal) != !q.Negative {
				isMatch = false
				break
			}
		}
		if isMatch {
			// dedup receivers and keep order
			if _, ok := receiverSet[r.Receiver]; !ok {
				receiverSet[r.Receiver] = struct{}{}
				receivers = append(receivers, r.Receiver)
			}
			if !r.Continue {
				break
			}
		}
	}
	return receivers, nil
}

func (m *Matcher) valid() error {
	for _, r := range m.Routes {
		for q := range r.Match {
			_, ok := m.Extractors[q.Label]
			if !ok {
				return fmt.Errorf("label %#v not found in extractors", q.Label)
			}
		}
	}
	return nil
}
