/*
 * 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/config"
	"axenov/iptv-checker/app/playlist"
	"context"
	"encoding/json"
	"math"
	"regexp"
	"sort"
	"strings"
	"time"
)

// PlaylistView — расширенное представление плейлиста для шаблонов
type PlaylistView struct {
	playlist.Playlist
	IsKnown        bool                   `json:"-"` // true если есть данные в кеше
	OnlinePercent  int                    `json:"-"`
	OfflinePercent int                    `json:"-"`
	HasCatchup     bool                   `json:"-"`
	HasTvg         bool                   `json:"-"`
	HasTokens      bool                   `json:"-"`
	Tags           []string               `json:"-"`
	ViewChannels   map[string]ChannelView `json:"-"`
}

// ChannelView — расширенное представление канала
type ChannelView struct {
	playlist.Channel
	HasToken bool `json:"hasToken"`
}

// PageData — данные для рендеринга шаблонов
type PageData struct {
	Title       string
	BaseUrl     string
	RepoUrl     string
	Version     string
	UpdatedAt   string
	UpdatedAtTZ string
	Meta        config.MetaConfig
	Menu        []config.Link
	FooterLinks []config.Link
	Tabs        config.TabsConfig
	Playlists   []PlaylistView
	Playlist    *PlaylistView
	Count       int
	PageCount   int
	PageCurrent int
	Code        string
	Error       string
}

// tokenPatterns — паттерны для определения нестабильных каналов
var tokenPatterns = []string{
	`[?&]token=`,
	`[?&]drmreq=`,
	`[?&]u=`,
	`[?&]user=`,
	`[?&]username=`,
	`[?&]p=`,
	`[?&]pwd=`,
	`[?&]password=`,
}

// enrichPlaylist подготавливает данные о плейлисте в расширенном формате,
// аналогично PHP IniFile::initPlaylist
func enrichPlaylist(view *PlaylistView) {
	// приколы golang: nil maps
	if view.Attributes == nil {
		view.Attributes = make(map[string]string)
	}
	if view.Groups == nil {
		view.Groups = make(map[string]playlist.Group)
	}
	if view.Channels == nil {
		view.Channels = make(map[string]playlist.Channel)
	}

	// проценты онлайн/оффлайн
	view.OnlinePercent = 0
	view.OfflinePercent = 0
	if view.IsKnown && view.IsOnline && len(view.Channels) > 0 {
		view.OnlinePercent = int(math.Round(float64(view.OnlineCount) / float64(len(view.Channels)) * 100))
		view.OfflinePercent = int(math.Round(float64(view.OfflineCount) / float64(len(view.Channels)) * 100))
	}

	// наличие catchup
	view.HasCatchup = strings.Contains(view.Content, "catchup")

	// наличие TVG
	view.HasTvg = view.Attributes["url-tvg"] != "" || view.Attributes["x-tvg-url"] != ""

	// наличие токенов в плейлисте
	view.HasTokens = hasTokensString(view.Url + " " + view.Content)

	// сбор тегов и токенов каналов
	view.ViewChannels = make(map[string]ChannelView, len(view.Channels))
	tagSet := make(map[string]bool)
	for id, ch := range view.Channels {
		cv := ChannelView{Channel: ch}
		cv.HasToken = hasTokensString(ch.URL)
		view.ViewChannels[id] = cv
		for _, tag := range ch.Tags {
			tagSet[tag] = true
		}
	}
	view.Tags = make([]string, 0, len(tagSet))
	for tag := range tagSet {
		view.Tags = append(view.Tags, tag)
	}
	sort.Strings(view.Tags)
}

// hasTokensString проверяет наличие токенов/ключей/логинов в строке
func hasTokensString(s string) bool {
	if s == "" {
		return false
	}
	for _, pattern := range tokenPatterns {
		if matched, _ := regexp.MatchString(pattern, s); matched {
			return true
		}
	}
	return false
}

// getPlaylistFromCache возвращает плейлист из кеша по коду
func (s *Server) getPlaylistFromCache(code string) (*PlaylistView, bool) {
	if s.cache == nil {
		return nil, false
	}
	ctx := context.Background()
	result, err := s.cache.Get(ctx, code).Result()
	if err != nil {
		return nil, false
	}

	var pls playlist.Playlist
	if err := json.Unmarshal([]byte(result), &pls); err != nil {
		return nil, false
	}

	view := &PlaylistView{Playlist: pls, IsKnown: true}
	enrichPlaylist(view)
	return view, true
}

// getPlaylistView возвращает представление плейлиста: из in-memory кеша, кеша или из ini
func (s *Server) getPlaylistView(code string) (*PlaylistView, bool) {
	// сначала пробуем in-memory кеш (результаты фоновой проверки)
	s.memMu.RLock()
	if pls, ok := s.memCache[code]; ok {
		s.memMu.RUnlock()
		view := &PlaylistView{Playlist: pls, IsKnown: true}
		enrichPlaylist(view)
		return view, true
	}
	s.memMu.RUnlock()

	// затем пробуем кеш
	if view, ok := s.getPlaylistFromCache(code); ok {
		return view, true
	}

	// если нет в кеше — берём метаданные из ini
	ini, err := s.loadIni()
	if err != nil {
		return nil, false
	}
	iniPls, exists := ini.Lists[code]
	if !exists {
		return nil, false
	}

	view := &PlaylistView{
		Playlist: playlist.Playlist{
			Code:        code,
			Name:        iniPls.Name,
			Description: iniPls.Description,
			Url:         iniPls.Url,
			Source:      iniPls.Source,
			IsOnline:    false,
			Attributes:  make(map[string]string),
			Groups:      make(map[string]playlist.Group),
			Channels:    make(map[string]playlist.Channel),
		},
		IsKnown:      false,
		Tags:         []string{},
		ViewChannels: make(map[string]ChannelView),
	}
	return view, true
}

// getPlaylistsView возвращает все плейлисты для списка (отсортированные по коду)
func (s *Server) getPlaylistsView() ([]PlaylistView, string, error) {
	ini, err := s.loadIni()
	if err != nil {
		return nil, "", err
	}

	updatedAt := s.getUpdatedAt()
	result := make([]PlaylistView, 0, len(ini.Lists))

	// собираем коды в порядке из ini-файла
	codes := make([]string, 0, len(ini.Lists))
	for _, section := range ini.File.Sections() {
		if section.Name() == "DEFAULT" {
			continue
		}
		if _, ok := ini.Lists[section.Name()]; ok {
			codes = append(codes, section.Name())
		}
	}

	// in-memory кеш (результаты фоновой проверки текущего запуска)
	s.memMu.RLock()
	memData := make(map[string]playlist.Playlist, len(s.memCache))
	for code, pls := range s.memCache {
		memData[code] = pls
	}
	s.memMu.RUnlock()

	// определяем коды, отсутствующие в in-memory — их берём из кеша
	var missingCodes []string
	for _, code := range codes {
		if _, ok := memData[code]; !ok {
			missingCodes = append(missingCodes, code)
		}
	}

	// кеш: пакетно читаем недостающие коды
	cacheData := make(map[string]playlist.Playlist)
	if s.cache != nil && len(missingCodes) > 0 {
		ctx := context.Background()
		cached, err := s.cache.MGet(ctx, missingCodes...).Result()
		if err == nil {
			for i, code := range missingCodes {
				if i < len(cached) {
					strVal, ok := cached[i].(string)
					if ok && strVal != "" {
						var pls playlist.Playlist
						if json.Unmarshal([]byte(strVal), &pls) == nil {
							cacheData[code] = pls
						}
					}
				}
			}
		}
	}

	// собираем результат: in-memory → кеш → unknown
	for _, code := range codes {
		iniPls := ini.Lists[code]
		if pls, ok := memData[code]; ok {
			view := PlaylistView{Playlist: pls, IsKnown: true}
			enrichPlaylist(&view)
			result = append(result, view)
		} else if pls, ok := cacheData[code]; ok {
			view := PlaylistView{Playlist: pls, IsKnown: true}
			enrichPlaylist(&view)
			result = append(result, view)
		} else {
			result = append(result, makeUnknownView(code, iniPls))
		}
	}
	return result, updatedAt, nil
}

// makeUnknownView создаёт представление непроверенного плейлиста
func makeUnknownView(code string, iniPls playlist.Playlist) PlaylistView {
	return PlaylistView{
		Playlist: playlist.Playlist{
			Code:        code,
			Name:        iniPls.Name,
			Description: iniPls.Description,
			Url:         iniPls.Url,
			Source:      iniPls.Source,
			IsOnline:    false,
			Attributes:  make(map[string]string),
			Groups:      make(map[string]playlist.Group),
			Channels:    make(map[string]playlist.Channel),
		},
		IsKnown:      false,
		Tags:         []string{},
		ViewChannels: make(map[string]ChannelView),
	}
}

// statusClass возвращает CSS-класс для статуса плейлиста
func statusClass(view *PlaylistView) string {
	if !view.IsKnown {
		return "secondary"
	}
	if view.IsOnline {
		return "success"
	}
	return "danger"
}

// statusBadge возвращает текст бейджа статуса
func statusBadge(view *PlaylistView) string {
	if !view.IsKnown {
		return "unknown"
	}
	if view.IsOnline {
		return "online"
	}
	return "offline"
}

// formatDate форматирует unix timestamp в строку
func formatDate(ts int64) string {
	if ts == 0 {
		return ""
	}
	return time.Unix(ts, 0).Format("02.01.2006 15:04:05")
}
