// Package engine — routing rules sub-engine.
//
// Персистентные правила маршрутизации (CID-based, destination, ingress-trunk)
// с priority-based first-match разрешением. Все match-поля — regex.
// Time-based match (дни недели, временной диапазон) применяется к текущему
// локальному времени сервера.
//
// Потокобезопасность: Router.mu защищает routing rules независимо от Engine.mu.
// PickNodeForCall не держит два лока одновременно.
package engine

import (
	"fmt"
	"math/rand/v2"
	"regexp"
	"sort"
	"strconv"
	"strings"
	"sync"
	"sync/atomic"
	"time"

	"github.com/pulse-lets-go/internal/models"
)

// CompiledRule — правило маршрутизации с прекомпилированными regex.
// hits — атомарный счётчик срабатываний (debug/мониторинг).
type CompiledRule struct {
	models.RouteRule            // экспортируемые JSON-поля
	hits             int64      // атомарный счётчик
	callerRe         *regexp.Regexp
	destRe           *regexp.Regexp
	ingressRe        *regexp.Regexp
}

// Hit атомарно инкрементирует счётчик срабатываний правила.
func (cr *CompiledRule) Hit() {
	atomic.AddInt64(&cr.hits, 1)
}

// GetHits возвращает текущее количество срабатываний.
func (cr *CompiledRule) GetHits() int64 {
	return atomic.LoadInt64(&cr.hits)
}

// Router хранит routing rules в порядке приоритета.
// Собственный sync.RWMutex изолирует contention между админом (редактирует правила) и трафиком (роутинг звонков).
type Router struct {
	mu    sync.RWMutex
	rules []*CompiledRule
}

// compileRules компилирует и сортирует правила.
// Возвращает ошибку если любой regex невалиден.
func compileRules(rules []models.RouteRule) ([]*CompiledRule, error) {
	compiled := make([]*CompiledRule, 0, len(rules))
	for _, r := range rules {
		cr := &CompiledRule{RouteRule: r}
		if r.MatchCallerID != "" {
			re, err := regexp.Compile(r.MatchCallerID)
			if err != nil {
				return nil, fmt.Errorf("rule %s (%s): invalid match_caller_id regex: %w", r.ID, r.Name, err)
			}
			cr.callerRe = re
		}
		if r.MatchDestination != "" {
			re, err := regexp.Compile(r.MatchDestination)
			if err != nil {
				return nil, fmt.Errorf("rule %s (%s): invalid match_destination regex: %w", r.ID, r.Name, err)
			}
			cr.destRe = re
		}
		if r.MatchIngressTrunk != "" {
			re, err := regexp.Compile(r.MatchIngressTrunk)
			if err != nil {
				return nil, fmt.Errorf("rule %s (%s): invalid match_ingress_trunk regex: %w", r.ID, r.Name, err)
			}
			cr.ingressRe = re
		}
		compiled = append(compiled, cr)
	}
	sort.Slice(compiled, func(i, j int) bool {
		return compiled[i].Priority < compiled[j].Priority
	})
	return compiled, nil
}

// ValidateRules проверяет что все правила валидны (regex, action_type, логические условия).
// Не изменяет состояние Engine. Используется для validate-before-save.
func (e *Engine) ValidateRules(rules []models.RouteRule) error {
	for _, r := range rules {
		if err := validateRule(&r); err != nil {
			return fmt.Errorf("rule %s (%s): %w", r.ID, r.Name, err)
		}
		// Проверяем что regex компилябельны
		if r.MatchCallerID != "" {
			if _, err := regexp.Compile(r.MatchCallerID); err != nil {
				return fmt.Errorf("rule %s (%s): invalid match_caller_id: %w", r.ID, r.Name, err)
			}
		}
		if r.MatchDestination != "" {
			if _, err := regexp.Compile(r.MatchDestination); err != nil {
				return fmt.Errorf("rule %s (%s): invalid match_destination: %w", r.ID, r.Name, err)
			}
		}
		if r.MatchIngressTrunk != "" {
			if _, err := regexp.Compile(r.MatchIngressTrunk); err != nil {
				return fmt.Errorf("rule %s (%s): invalid match_ingress_trunk: %w", r.ID, r.Name, err)
			}
		}
	}
	return nil
}

func validateRule(r *models.RouteRule) error {
	switch r.ActionType {
	case "node":
		if r.ActionNodeID == "" {
			return fmt.Errorf("action_type 'node' requires action_node_id")
		}
	case "pool":
		if len(r.ActionPoolIDs) == 0 {
			return fmt.Errorf("action_type 'pool' requires action_pool_ids")
		}
	case "auto":
		// no extra requirements
	default:
		return fmt.Errorf("action_type must be 'node', 'pool' or 'auto'")
	}

	// Хотя бы одно match-поле должно быть непустым
	if r.MatchCallerID == "" && r.MatchDestination == "" &&
		r.MatchIngressTrunk == "" && r.MatchDaysOfWeek == "" &&
		r.MatchTimeStart == "" && r.MatchTimeEnd == "" {
		return fmt.Errorf("at least one match field required")
	}

	// Time range: оба поля или ни одного
	if (r.MatchTimeStart != "" && r.MatchTimeEnd == "") ||
		(r.MatchTimeStart == "" && r.MatchTimeEnd != "") {
		return fmt.Errorf("both match_time_start and match_time_end required together")
	}

	return nil
}

// LoadRules компилирует правила из моделей и загружает в in-memory Router.
// Используется при запуске: битые правила логируются и скипаются,
// валидные загружаются.
func (e *Engine) LoadRules(rules []models.RouteRule) {
	compiled, err := compileRules(rules)
	if err != nil {
		// Логируем ошибку, но не прерываем — загружаем что смогли
		_ = err // caller logs
	}
	e.router.mu.Lock()
	e.router.rules = compiled
	e.router.mu.Unlock()
}

// SetRules загружает ПРОВЕРЕННЫЕ правила в in-memory Router.
// Вызывается после ValidateRules + сохранения на диск.
// Правила считаются уже валидными.
func (e *Engine) SetRules(rules []models.RouteRule) {
	compiled, err := compileRules(rules)
	if err != nil {
		// Не должно случиться (rules уже прошли ValidateRules)
		return
	}
	e.router.mu.Lock()
	e.router.rules = compiled
	e.router.mu.Unlock()
}

// GetCompiledRules возвращает текущий срез скомпилированных правил.
func (e *Engine) GetCompiledRules() []*CompiledRule {
	e.router.mu.RLock()
	defer e.router.mu.RUnlock()
	rules := make([]*CompiledRule, len(e.router.rules))
	copy(rules, e.router.rules)
	return rules
}

// matchRule проверяет все условия правила против параметров вызова.
// Возвращает true если правило применимо (все непустые поля совпали).
// Вызывающий НЕ держит router-блокировку (она должна быть внешней).
func matchRule(cr *CompiledRule, callerID, dest, ingress string) bool {
	if !cr.Enabled {
		return false
	}
	if cr.callerRe != nil && !cr.callerRe.MatchString(callerID) {
		return false
	}
	if cr.destRe != nil && !cr.destRe.MatchString(dest) {
		return false
	}
	if cr.ingressRe != nil && !cr.ingressRe.MatchString(ingress) {
		return false
	}
	return matchTime(cr.MatchDaysOfWeek, cr.MatchTimeStart, cr.MatchTimeEnd)
}

// matchTime проверяет time-based условия правила.
// Использует локальное время сервера.
func matchTime(daysOfWeek, timeStart, timeEnd string) bool {
	if daysOfWeek == "" && timeStart == "" && timeEnd == "" {
		return true
	}

	now := time.Now()

	// Проверка дня недели
	if daysOfWeek != "" {
		if !matchDayOfWeek(now, daysOfWeek) {
			return false
		}
	}

	// Проверка временного диапазона
	if timeStart != "" && timeEnd != "" {
		if !matchTimeRange(now, timeStart, timeEnd) {
			return false
		}
	}

	return true
}

// matchDayOfWeek проверяет что текущий день недели входит в заданный диапазон/список.
// Формат: "1-5" (пн-пт), "1,3,5" (пн,ср,пт), "6" (сб).
// Воскресенье = 0 или 7. Понедельник = 1.
func matchDayOfWeek(now time.Time, spec string) bool {
	wd := now.Weekday()
	if wd == time.Sunday {
		// Приводим воскресенье к 7 для совместимости с "1-7"
		wd = 7
	}
	dayNum := int(wd)

	for _, part := range strings.Split(spec, ",") {
		part = strings.TrimSpace(part)
		if part == "" {
			continue
		}
		if strings.Contains(part, "-") {
			parts := strings.SplitN(part, "-", 2)
			start, err1 := strconv.Atoi(strings.TrimSpace(parts[0]))
			end, err2 := strconv.Atoi(strings.TrimSpace(parts[1]))
			if err1 == nil && err2 == nil && dayNum >= start && dayNum <= end {
				return true
			}
		} else {
			n, err := strconv.Atoi(part)
			if err == nil && dayNum == n {
				return true
			}
		}
	}
	return false
}

// matchTimeRange проверяет что текущее время входит в [timeStart, timeEnd].
// Формат: "HH:MM".
func matchTimeRange(now time.Time, timeStart, timeEnd string) bool {
	startH, startM, ok1 := parseHHMM(timeStart)
	endH, endM, ok2 := parseHHMM(timeEnd)
	if !ok1 || !ok2 {
		return false
	}

	startMin := startH*60 + startM
	endMin := endH*60 + endM
	nowMin := now.Hour()*60 + now.Minute()

	if startMin <= endMin {
		// Обычный диапазон: 09:00 – 18:00
		return nowMin >= startMin && nowMin <= endMin
	}
	// Ночной диапазон: 22:00 – 06:00
	return nowMin >= startMin || nowMin <= endMin
}

func parseHHMM(s string) (h, m int, ok bool) {
	parts := strings.SplitN(s, ":", 2)
	if len(parts) != 2 {
		return 0, 0, false
	}
	hour, err1 := strconv.Atoi(parts[0])
	minute, err2 := strconv.Atoi(parts[1])
	if err1 != nil || err2 != nil || hour < 0 || hour > 23 || minute < 0 || minute > 59 {
		return 0, 0, false
	}
	return hour, minute, true
}

// PickNodeForCall — единый фасад маршрутизации вызова с учётом routing rules.
//
// Алгоритм:
//  1. Проверяем routing rules в порядке Priority (first-match-wins)
//  2. Для правила с action="node" — проверяем что нода жива; если мертва — fallthrough
//  3. Для правила с action="pool" — scoring только по нодам из пула; пустой пул → fallthrough
//  4. Для правила с action="auto" — fallthrough к глобальному PickNode()
//  5. Нет совпадений / fallthrough — стандартный PickNode() (глобальный scoring)
//
// matchedRule возвращает имя сработавшего правила (или "none").
func (e *Engine) PickNodeForCall(callerID, dest, ingress string) (nodeID string, score float64, fallback bool, matchedRule string) {
	e.router.mu.RLock()
	defer e.router.mu.RUnlock()

	for _, cr := range e.router.rules {
		if !matchRule(cr, callerID, dest, ingress) {
			continue
		}
		cr.Hit()

		resolved := false
		switch cr.ActionType {
		case "node":
			e.mu.RLock()
			ns, ok := e.nodes[cr.ActionNodeID]
			now := time.Now().Unix()
			staleThreshold := int64(e.cfg.StaleThresholdSec)
			e.mu.RUnlock()
			if ok && ns.Score >= 0 && now-ns.TS <= staleThreshold {
				nodeID, score, matchedRule = cr.ActionNodeID, 100, cr.Name
				resolved = true
			}
		case "pool":
			nid, sc, fb := e.pickNodeFromPoolLocked(cr.ActionPoolIDs)
			if !fb {
				nodeID, score, matchedRule = nid, sc, cr.Name
				resolved = true
			}
		case "auto":
			// Fallthrough к глобальному scoring
			nid, sc, fb := e.pickNodeLocked()
			return nid, sc, fb, cr.Name
		}
		if resolved {
			return nodeID, score, false, matchedRule
		}
	}

	// Нет совпадений / все правила провалились — стандартный scoring
	nid, sc, fb := e.pickNodeLocked()
	return nid, sc, fb, "none"
}

// pickNodeLocked возвращает ноду согласно активной стратегии балансировки.
// Вызывающий ДОЛЖЕН держать e.router.mu.RLock (не engine.mu).
func (e *Engine) pickNodeLocked() (nodeID string, score float64, fallback bool) {
	e.mu.RLock()
	defer e.mu.RUnlock()

	switch e.balanceMode {
	case "weighted_random":
		return e.pickWeightedRandomLocked()
	default:
		return e.pickBestNodeLocked()
	}
}

// pickNodeFromPoolLocked выполняет scoring только по нодам из пула (под router.RLock).
func (e *Engine) pickNodeFromPoolLocked(poolIDs []string) (nodeID string, score float64, fallback bool) {
	e.mu.RLock()
	defer e.mu.RUnlock()
	return e.pickNodeFromPoolUnlocked(poolIDs)
}

// pickNodeFromPoolUnlocked — внутренний метод без блокировок (engine.mu уже держится).
func (e *Engine) pickNodeFromPoolUnlocked(poolIDs []string) (nodeID string, score float64, fallback bool) {

	if len(e.nodes) == 0 {
		return "", 0, true
	}

	poolSet := make(map[string]bool, len(poolIDs))
	for _, id := range poolIDs {
		poolSet[id] = true
	}

	now := time.Now().Unix()
	staleThreshold := int64(e.cfg.StaleThresholdSec)

	type candidate struct {
		id    string
		score float64
	}
	candidates := make([]candidate, 0)

	for id, ns := range e.nodes {
		if !poolSet[id] {
			continue
		}
		if ns.Disabled {
			continue
		}
		if ns.Score < 0 {
			continue
		}
		if now-ns.TS > staleThreshold {
			continue
		}
		candidates = append(candidates, candidate{id: id, score: ns.Score})
	}

	if len(candidates) == 0 {
		return "", 0, true
	}

	switch e.balanceMode {
	case "best":
		// Найти ноду с максимальным score
		best := candidates[0]
		for _, c := range candidates[1:] {
			if c.score > best.score {
				best = c
			}
		}
		return best.id, best.score, false
	default:
		// Weighted random
		var totalScore float64
		for _, c := range candidates {
			totalScore += c.score
		}
		if totalScore <= 0 {
			idx := rand.IntN(len(candidates))
			return candidates[idx].id, 0, false
		}
		pick := rand.Float64() * totalScore
		for _, c := range candidates {
			pick -= c.score
			if pick <= 0 {
				return c.id, c.score, false
			}
		}
		return candidates[len(candidates)-1].id, candidates[len(candidates)-1].score, false
	}
}
