// pulse-lets-go — балансировщик телефонной нагрузки на базе FreeSWITCH + NATS.
// Принимает метрики от нижестоящих АТС, вычисляет score готовности,
// возвращает лучшую ноду для следующего вызова через /api/route.
package main

import (
	"context"
	"fmt"
	"log"
	"net/http"
	"os"
	"os/signal"
	"path/filepath"
	"strings"
	"syscall"
	"time"

	"github.com/pulse-lets-go/internal/api"
	"github.com/pulse-lets-go/internal/config"
	"github.com/pulse-lets-go/internal/engine"
	"github.com/pulse-lets-go/internal/esl"
	filelog "github.com/pulse-lets-go/internal/log"
	"github.com/pulse-lets-go/internal/models"
	"github.com/pulse-lets-go/internal/nats"
	"github.com/pulse-lets-go/internal/util"
)

func main() {
	// Определяем рабочую директорию
	// По умолчанию data/ — родительская папка от бинарника.
	// Пример: /opt/pulse-lets-go/bin/pulse-lets-go → /opt/pulse-lets-go/data
	exe, _ := os.Executable()
	exeDir := filepath.Dir(exe)
	dataDir := filepath.Join(filepath.Dir(exeDir), "data")

	if envDir := os.Getenv("PULSE_DATA_DIR"); envDir != "" {
		dataDir = envDir
	}

	log.SetFlags(log.LstdFlags | log.Lshortfile)
	log.Printf("[main] запуск pulse-lets-go, data_dir=%s", dataDir)

	// 1. Инициализация конфиг-менеджера (создаёт data/ и дефолтные JSON при первом запуске)
	cfgMgr, err := config.NewManager(dataDir)
	if err != nil {
		log.Fatalf("[main] ошибка инициализации конфигов: %v", err)
	}

	// 2. Чтение конфига
	cfg, err := cfgMgr.ReadConfig()
	if err != nil {
		log.Fatalf("[main] ошибка чтения config.json: %v", err)
	}

	// 2.1 Валидация конфига
	if err := cfg.Validate(); err != nil {
		log.Fatalf("[main] ошибка валидации конфига:\n%v", err)
	}

	log.Printf("[main] NATS=%s, listen=%s, stale_threshold=%dсек, tls=%v, log=%s/%s",
		cfg.NatsURL, cfg.ListenAddr, cfg.StaleThresholdSec, cfg.TLS.Enabled, cfg.Log.Level, cfg.Log.Format)

	// 3. Engine
	eng := engine.NewEngine(cfg)

	// 3.1 Загружаем routing rules из routing.json (graceful degradation: битые правила скипаются)
	rules, err := cfgMgr.ReadRoutingRules()
	if err != nil {
		log.Printf("[main] ошибка чтения routing.json: %v (работаем без правил)", err)
	} else if len(rules) > 0 {
		eng.LoadRules(rules)
		log.Printf("[main] загружено %d правил маршрутизации из routing.json", len(rules))
	}

	// 3.2 Запускаем эвикцию stale-нод (очистка нод без метрик > 5 минут, проверка каждые 60 сек)
	eng.StartEvictionLoop(60*time.Second, 5*time.Minute)

	// 4. ASCII логгер метрик
	metricsLogger, err := filelog.NewLogger(dataDir)
	if err != nil {
		log.Fatalf("[main] ошибка создания логгера: %v", err)
	}

	// 5. NATS подписчик
	sub, err := nats.NewSubscriber(cfg.NatsURL, cfg.NatsUser, cfg.NatsPassword, eng, metricsLogger)
	if err != nil {
		log.Fatalf("[main] ошибка NATS: %v", err)
	}
	defer sub.Close()

	var apiHandler *api.API

	// Авто-создание или обновление balance-транка при метрике от ноды
	sub.SetOnNewNode(func(nodeID, sipGateway string) {
		trunks, err := cfgMgr.ReadTrunks()
		if err != nil {
			log.Printf("[main] ошибка чтения транков для авто-создания: %v", err)
			return
		}
		// Ищем существующий balance-транк для этой ноды
		for i, t := range trunks {
			if t.Type == "balance" && t.NodeID == nodeID {
				if t.Gateway != sipGateway {
					trunks[i].Gateway = sipGateway
					trunks[i].UpdatedAt = time.Now().UTC()
					if err := cfgMgr.SaveTrunks(trunks); err != nil {
						log.Printf("[main] ошибка обновления gateway транка для %s: %v", nodeID, err)
						return
					}
					apiHandler.RebuildTrunkCache()
					log.Printf("[nats] обновлён gateway транка для ноды %s → %s", nodeID, sipGateway)
				}
				return
			}
		}
		// Создаём новый balance-транк
		now := time.Now().UTC()
		trunkID := "trk-" + util.RandomHex(6)
		trunk := models.Trunk{
			ID:        trunkID,
			Name:      fmt.Sprintf("%s баланс", nodeID),
			Type:      "balance",
			NodeID:    nodeID,
			Gateway:   sipGateway,
			Codecs:    []string{"PCMU", "PCMA"},
			Context:   "default",
			Enabled:   true,
			CreatedAt: now,
			UpdatedAt: now,
		}
		trunks = append(trunks, trunk)
		if err := cfgMgr.SaveTrunks(trunks); err != nil {
			log.Printf("[main] ошибка сохранения авто-созданного транка: %v", err)
			return
		}
		apiHandler.RebuildTrunkCache()
		log.Printf("[nats] авто-создан balance-транк: %s → %s (узел: %s)", trunkID, sipGateway, nodeID)
	})

	// 6. HTTP API
	apiHandler = api.NewAPI(eng, cfgMgr, cfg.GetJWTSecret(), cfg.MonitoringAPIKey, sub.IsConnected, cfg)
	apiHandler.SetRouteCachePath(filepath.Join(dataDir, "route_cache.json"))
	apiHandler.RebuildTrunkCache() // первичное построение in-memory кэша gateway

	sub.SetOnMetric(func() {
		apiHandler.PushRouteCache()
	})

	// При эвикции stale-ноды — удаляем её balance-транк из trunks.json
	eng.SetOnNodeEvicted(func(nodeID string) {
		trunks, err := cfgMgr.ReadTrunks()
		if err != nil {
			log.Printf("[main] ошибка чтения транков для очистки evicted ноды %s: %v", nodeID, err)
			return
		}
		filtered := make([]models.Trunk, 0, len(trunks))
		for _, t := range trunks {
			if t.Type == models.TrunkBalance && t.NodeID == nodeID {
				continue
			}
			filtered = append(filtered, t)
		}
		if len(filtered) == len(trunks) {
			return
		}
		if err := cfgMgr.SaveTrunks(filtered); err != nil {
			log.Printf("[main] ошибка сохранения транков после eviction %s: %v", nodeID, err)
			return
		}
		apiHandler.RebuildTrunkCache()
		log.Printf("[main] удалён balance-транк для evicted ноды %s", nodeID)
	})
	handler := apiHandler.Handler()

	// 6.1 ESL-клиент (опционально — если сконфигурирован)
	var eslClient *esl.Client
	if cfg.ESL.Host != "" {
		eslPassword := cfg.ESL.GetPassword()
		eslClient = esl.NewClient(cfg.ESL.Host, cfg.ESL.Port, eslPassword)
		apiHandler.SetESLClient(eslClient)

		ctx, cancel := context.WithCancel(context.Background())
		go func() {
			eslClient.StartEventLoop(ctx, cfg.ESL.SofiaProfile,
				func() {
					trunks, err := cfgMgr.ReadTrunks()
					if err != nil {
						log.Printf("[esl] ошибка чтения транков для синхронизации: %v", err)
						return
					}
					var specs []esl.GatewaySpec
					for _, t := range trunks {
						if t.Type == "ingress" && t.Enabled {
							specs = append(specs, esl.GatewaySpec{
								Name:  fmt.Sprintf("pulse-ingress-%s", t.ID),
								Proxy: extractHostFromGateway(t.Gateway),
							})
						}
					}
					eslClient.GatewaySyncAll(cfg.ESL.SofiaProfile, specs)
				},
				func(eventName string, headers map[string]string, body string) {
					gwName := headers["Gateway-Name"]
					if gwName == "" {
						return
					}
					// Извлекаем trunk_id из имени gateway: "pulse-ingress-trk-X" → "trk-X"
					prefix := cfg.ESL.GatewayPrefix + "-ingress-"
					trunkID := strings.TrimPrefix(gwName, prefix)
					if trunkID == gwName {
						return // префикс не найден, не наш gateway
					}

					status := "unknown"
					switch eventName {
					case "SOFIA::gateway_register":
						status = "up"
					case "SOFIA::gateway_unregister", "SOFIA::gateway_expire":
						status = "down"
					}

					apiHandler.BroadcastGatewayEvent(gwName, trunkID, status)
				},
			)
			cancel()
		}()
	}

	// ESL shutdown при завершении main
	defer func() {
		if eslClient != nil {
			eslClient.Disconnect()
		}
	}()

	// 6.2 Periodic WS broadcast + FS stats poll
	go func() {
		ticker := time.NewTicker(5 * time.Second)
		for range ticker.C {
			apiHandler.PushRouteCache()
			if eslClient != nil && eslClient.IsConnected() {
				if stats, err := eslClient.FetchFsStats(); err == nil {
					apiHandler.UpdateFsStats(stats)
				}
			}
			apiHandler.BroadcastAll()
		}
	}()

	// Подключаем раздачу SvelteKit статики, если директория существует
	webDir := findWebDir(exeDir)
	if webDir != "" {
		log.Printf("[main] раздача SvelteKit статики из %s", webDir)
		handler = withSPA(webDir, handler)
	}

	srv := &http.Server{
		Addr:         cfg.ListenAddr,
		Handler:      handler,
		ReadTimeout:  10 * time.Second,
		WriteTimeout: 10 * time.Second,
		IdleTimeout:  60 * time.Second,
	}

	// 7. Запуск HTTP-сервера
	go func() {
		log.Printf("[main] HTTP сервер запущен на %s (tls=%v)", cfg.ListenAddr, cfg.TLS.Enabled)
		log.Printf("[main] готов к работе:")
		log.Printf("  GET  /api/health          — статус зависимостей")
		log.Printf("  POST /api/auth/login       — аутентификация")
		log.Printf("  GET  /api/route            — маршрутизация (FreeSWITCH)")
		log.Printf("  GET  /api/nodes            — список нод")
		log.Printf("  GET  /api/trunks           — управление транками")
		log.Printf("  GET  /api/users            — управление пользователями")
		log.Printf("  GET  /api/routing/rules   — правила маршрутизации (CID/dest/trunk/time)")
		log.Printf("  GET  /api/monitoring/zabbix  — Zabbix LLD + items")
		log.Printf("  GET  /api/monitoring/prometheus — Prometheus метрики")
		log.Printf("  WS   /ws/metrics           — real-time метрики на фронт")

		var err error
		if cfg.TLS.Enabled {
			err = srv.ListenAndServeTLS(cfg.TLS.CertFile, cfg.TLS.KeyFile)
		} else {
			err = srv.ListenAndServe()
		}
		if err != nil && err != http.ErrServerClosed {
			log.Fatalf("[main] ошибка HTTP сервера: %v", err)
		}
	}()

	// 8. Graceful shutdown
	quit := make(chan os.Signal, 1)
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
	sig := <-quit
	log.Printf("[main] получен сигнал %s, завершение работы...", sig)

	eng.StopEvictionLoop()

	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	if err := srv.Shutdown(ctx); err != nil {
		log.Printf("[main] ошибка остановки HTTP сервера: %v", err)
	}

	metricsLogger.Close()

	log.Println("[main] pulse-lets-go остановлен")
}

// findWebDir ищет директорию со SvelteKit сборкой (web/build/).
// Пробует по порядку: PULSE_WEB_DIR, CWD, ../web/build, рядом с бинарником.
func findWebDir(exeDir string) string {
	candidates := []string{
		os.Getenv("PULSE_WEB_DIR"),
		"web/build", // ./web/build (CWD — dev, systemd working dir)
		filepath.Join(filepath.Dir(exeDir), "web", "build"), // ../web/build (корень проекта, dev с bin/)
		filepath.Join(exeDir, "web", "build"),               // рядом с бинарником (production install, make deploy)
	}
	for _, d := range candidates {
		if d == "" {
			continue
		}
		if stat, err := os.Stat(d); err == nil && stat.IsDir() {
			return d
		}
	}
	return ""
}

// withSPA — middleware для раздачи SvelteKit SPA.
// Запросы к /api/* и /ws/* проксируются в API handler.
// Остальные запросы ищут статический файл в webDir; если файл не найден,
// возвращается index.html (SPA fallback для клиентского роутинга).
func withSPA(webDir string, apiHandler http.Handler) http.Handler {
	fs := http.FileServer(http.Dir(webDir))

	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		path := r.URL.Path

		// API и WebSocket запросы — передаём в API
		if strings.HasPrefix(path, "/api/") || strings.HasPrefix(path, "/ws/") {
			apiHandler.ServeHTTP(w, r)
			return
		}

		// Пробуем отдать статический файл
		filePath := filepath.Join(webDir, path)
		info, err := os.Stat(filePath)

		// Если файл существует и не является директорией — отдаём
		if err == nil && !info.IsDir() {
			fs.ServeHTTP(w, r)
			return
		}

		// SPA fallback: index.html
		indexPath := filepath.Join(webDir, "index.html")
		if _, err := os.Stat(indexPath); err == nil {
			http.ServeFile(w, r, indexPath)
			return
		}

		// Если index.html тоже нет — 404
		http.NotFound(w, r)
	})
}

// extractHostFromGateway извлекает хост из SIP URI.
// "sip:mts-gw.lan:5060" → "mts-gw.lan"
func extractHostFromGateway(gateway string) string {
	s := strings.TrimPrefix(gateway, "sip:")
	s = strings.TrimPrefix(s, "sips:")
	if idx := strings.LastIndex(s, ":"); idx > 0 {
		port := s[idx+1:]
		isNumeric := true
		for _, c := range port {
			if c < '0' || c > '9' {
				isNumeric = false
				break
			}
		}
		if isNumeric {
			s = s[:idx]
		}
	}
	return s
}
