/*
 * Copyright (c) 2025-2026, Антон Аксенов
 * This file is part of iptvc project
 * MIT License: https://git.axenov.dev/IPTV/iptvc/src/branch/master/LICENSE
 */

package web

import (
	"axenov/iptv-checker/app/checker"
	"axenov/iptv-checker/app/config"
	"axenov/iptv-checker/app/inifile"
	"axenov/iptv-checker/app/playlist"
	"axenov/iptv-checker/app/utils"
	"log"
	"maps"
	"net/http"
	"os"
	"slices"
	"strconv"
	"sync"
	"time"

	"github.com/redis/go-redis/v9"
)

// Server — веб-сервер для отображения плейлистов
type Server struct {
	cfg       *config.Config
	cache     *redis.Client
	templates *TemplateManager
	iniCache  inifile.IniFile
	iniLoaded time.Time
	memMu     sync.RWMutex
	memCache  map[string]playlist.Playlist
}

// NewServer создаёт новый экземпляр веб-сервера
func NewServer(cfg *config.Config, cache *redis.Client) *Server {
	s := &Server{
		cfg:      cfg,
		cache:    cache,
		memCache: make(map[string]playlist.Playlist),
	}

	tm, err := NewTemplateManager(cfg)
	if err != nil {
		log.Fatalf("Failed to load templates: %s", err)
	}
	s.templates = tm

	return s
}

// Start запускает веб-сервер
func (s *Server) Start() {
	mux := http.NewServeMux()

	// API routes (specific, no conflicts)
	mux.HandleFunc("GET /api/playlists", s.handleAPIGetAll)
	mux.HandleFunc("GET /api/playlists/{code}", s.handleAPIGetOne)
	mux.HandleFunc("GET /api/playlists/{code}/channels", s.handleAPIGetChannels)
	mux.HandleFunc("GET /api/version", s.handleAPIVersion)
	mux.HandleFunc("GET /api/health", s.handleAPIHealth)
	mux.HandleFunc("GET /api/stats", s.handleAPIStats)

	// Static files
	mux.Handle("GET /static/", http.StripPrefix("/static/", staticFileServer))

	// OpenAPI spec + Swagger UI
	mux.HandleFunc("GET /api/openapi.json", s.handleOpenAPISpec)
	mux.HandleFunc("GET /api", func(w http.ResponseWriter, r *http.Request) {
		http.Redirect(w, r, "/api/", http.StatusMovedPermanently)
	})
	mux.Handle("GET /api/", http.StripPrefix("/api/", swaggerFileServer))

	// All other routes handled by catch-all to avoid mux pattern conflicts
	mux.HandleFunc("GET /{$}", s.handleHome)
	mux.HandleFunc("GET /{path...}", s.handleRoute)

	handler := s.withMiddleware(mux)

	addr := s.cfg.Server.Host + ":" + strconv.FormatUint(uint64(s.cfg.Server.Port), 10)
	log.Printf("Starting web server on %s", addr)
	if err := http.ListenAndServe(addr, handler); err != nil {
		log.Fatalf("Server failed: %s", err)
	}
}

// withMiddleware добавляет middleware: логирование, восстановление после паники
func (s *Server) withMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		start := time.Now()
		defer func() {
			if err := recover(); err != nil {
				log.Printf("PANIC: %v", err)
				http.Error(w, "Internal Server Error", http.StatusInternalServerError)
			}
		}()
		next.ServeHTTP(w, r)
		log.Printf("%s %s %s %s", r.Method, r.URL.Path, time.Since(start), r.RemoteAddr)
	})
}

// loadIni загружает ini-файл (с кешированием на 30 секунд)
func (s *Server) loadIni() (inifile.IniFile, error) {
	// перезагружаем раз в 30 секунд
	if !s.iniLoaded.IsZero() && time.Since(s.iniLoaded) < 30*time.Second && len(s.iniCache.Lists) > 0 {
		return s.iniCache, nil
	}

	ini, err := inifile.Init(s.cfg.App.Playlists)
	if err != nil {
		return inifile.IniFile{}, err
	}
	s.iniCache = ini
	s.iniLoaded = time.Now()
	return ini, nil
}

// getUpdatedAt возвращает время последнего изменения ini-файла
func (s *Server) getUpdatedAt() string {
	path, err := utils.ExpandPath(s.cfg.App.Playlists)
	if err != nil {
		return ""
	}
	info, err := os.Stat(path)
	if err != nil {
		return ""
	}
	loc, err := time.LoadLocation(s.cfg.App.Timezone)
	if err != nil {
		loc = time.UTC
	}
	return info.ModTime().In(loc).Format("02.01.2006 15:04")
}

// getTimezoneLabel возвращает сокращённое обозначение часового пояса для отображения.
func (s *Server) getTimezoneLabel() string {
	return s.cfg.App.Timezone
}

// CheckOptions — параметры фоновой проверки плейлистов
type CheckOptions struct {
	Repeat uint
	Random uint
	Files  []string
	Urls   []string
	Codes  []string
}

// StartBackgroundChecker запускает фоновую периодическую проверку плейлистов.
// Метод блокирует вызывающую горутину, поэтому должен запускаться в отдельной goroutine.
func (s *Server) StartBackgroundChecker(opts CheckOptions) {
	// каждый проверенный плейлист сразу попадает в in-memory кеш
	checker.OnPlaylistChecked = func(pls playlist.Playlist) {
		s.memMu.Lock()
		s.memCache[pls.Code] = pls
		s.memMu.Unlock()
		// сбрасываем кеш ini, чтобы веб-часть подхватила свежие данные
		s.iniLoaded = time.Time{}
	}

	// AllCooldown хранится в миллисекундах, поэтому переводим в time.Millisecond
	cooldown := time.Duration(s.cfg.Check.Playlists.AllCooldown.Value()) * time.Millisecond
	log.Printf("Background checker started: cooldown=%s repeat=%d", cooldown, opts.Repeat)

	iteration := uint(0)
	for {
		iteration++
		log.Printf("@ Background checker iteration=%d repeat=%d", iteration, opts.Repeat)

		s.runCheckerOnce(opts)

		if opts.Repeat > 0 && iteration >= opts.Repeat {
			log.Println("Background checker finished: reached repeat count")
			return
		}

		log.Printf("Background checker waiting... cooldown=%s", cooldown)
		// cooldown уже вычислен в миллисекундах выше
		time.Sleep(cooldown)
	}
}

// runCheckerOnce выполняет один цикл проверки плейлистов
func (s *Server) runCheckerOnce(opts CheckOptions) {
	defer func() {
		if err := recover(); err != nil {
			log.Printf("Background checker panic: %v", err)
		}
	}()

	var lists []playlist.Playlist

	if len(opts.Files) > 0 || len(opts.Urls) > 0 || len(opts.Codes) > 0 {
		// проверка конкретных файлов/URL/кодов
		lists = checker.PrepareListsToCheck(opts.Files, opts.Urls, opts.Codes)
	} else {
		// проверка всех плейлистов из ini
		ini, err := inifile.Init(s.cfg.App.Playlists)
		if err != nil {
			log.Printf("Background checker: cannot load ini: %s", err)
			return
		}
		lists = slices.Collect(maps.Values(ini.Lists))
	}

	if len(lists) == 0 {
		log.Println("Background checker: no playlists to check")
		return
	}

	log.Printf("Background checker: %d playlists will be checked", len(lists))
	startTime := time.Now()
	onlineCount, offlineCount := checker.CheckPlaylists(lists)
	log.Printf(
		"Background checker done! online=%d offline=%d elapsed=%.2fs",
		onlineCount,
		offlineCount,
		time.Since(startTime).Seconds(),
	)
}
