/*
 * 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"
	"axenov/iptv-checker/app/playlist"
	"encoding/json"
	"fmt"
	"log"
	"math"
	"net/http"
	"strconv"
	"strings"
	"time"
)

// handleHome — главная страница со списком плейлистов
func (s *Server) handleHome(w http.ResponseWriter, r *http.Request) {
	playlists, updatedAt, err := s.getPlaylistsView()
	errorMsg := ""
	if err != nil {
		log.Printf("Error loading playlists: %s", err)
		errorMsg = "Не удалось загрузить список плейлистов. Проверьте наличие файла playlists.ini."
		playlists = []PlaylistView{}
		updatedAt = ""
	}

	count := len(playlists)
	pageSize := int(s.cfg.Site.PageSize)
	pageCurrent := 1
	pageCount := 1

	// pagination
	if pageSize > 0 && count > pageSize {
		pageCount = int(math.Ceil(float64(count) / float64(pageSize)))
		if pageArg := r.PathValue("page"); pageArg != "" {
			if p, err := strconv.Atoi(pageArg); err == nil && p >= 1 && p <= pageCount {
				pageCurrent = p
			}
		}
		offset := (pageCurrent - 1) * pageSize
		if offset >= count {
			offset = 0
			pageCurrent = 1
		}
		end := offset + pageSize
		if end > count {
			end = count
		}
		playlists = playlists[offset:end]
	}

	data := &PageData{
		Title:       s.cfg.Site.Title,
		BaseUrl:     s.cfg.Site.BaseUrl,
		RepoUrl:     s.cfg.Site.RepoUrl,
		Version:     app.VERSION,
		UpdatedAt:   updatedAt,
		UpdatedAtTZ: s.getTimezoneLabel(),
		Meta:        s.cfg.Site.Meta,
		Menu:        s.cfg.Site.Header.Menu,
		FooterLinks: s.cfg.Site.Footer.Links,
		Tabs:        s.cfg.Site.Tabs,
		Playlists:   playlists,
		Count:       count,
		PageCount:   pageCount,
		PageCurrent: pageCurrent,
		Error:       errorMsg,
	}

	s.templates.Render(w, "list", data)
}

// handleDetails — страница с описанием плейлиста
func (s *Server) handleDetails(w http.ResponseWriter, r *http.Request) {
	code := r.PathValue("code")
	if code == "" {
		s.renderNotFound(w, r, "")
		return
	}

	view, ok := s.getPlaylistView(code)
	if !ok {
		s.renderNotFound(w, r, code)
		return
	}

	data := &PageData{
		Title:       s.cfg.Site.Title,
		BaseUrl:     s.cfg.Site.BaseUrl,
		RepoUrl:     s.cfg.Site.RepoUrl,
		Version:     app.VERSION,
		Meta:        s.cfg.Site.Meta,
		Menu:        s.cfg.Site.Header.Menu,
		FooterLinks: s.cfg.Site.Footer.Links,
		Tabs:        s.cfg.Site.Tabs,
		Playlist:    view,
	}

	s.templates.Render(w, "details", data)
}

// handleRoute — единый обработчик для не-API маршрутов
// Разбирает путь и направляет на нужный обработчик:
// /page/N → главная с пагинацией
// /CODE/details → страница плейлиста
// /CODE или /CODE.m3u[8] → редирект на URL плейлиста
// остальное → 404
func (s *Server) handleRoute(w http.ResponseWriter, r *http.Request) {
	path := r.PathValue("path")
	if path == "" {
		s.handleHome(w, r)
		return
	}

	segments := strings.Split(path, "/")

	switch len(segments) {
	case 1:
		// /page/N → пагинация
		if segments[0] == "page" {
			s.renderNotFound(w, r, "")
			return
		}
		if strings.HasPrefix(segments[0], "page/") {
			// shouldn't happen with single segment, but just in case
			pageStr := strings.TrimPrefix(segments[0], "page/")
			if _, err := strconv.Atoi(pageStr); err == nil {
				r.SetPathValue("page", pageStr)
				s.handleHome(w, r)
				return
			}
		}
		// /CODE or /CODE.m3u[8] → redirect
		s.handleRedirect(w, r, segments[0])

	case 2:
		// /page/N → пагинация
		if segments[0] == "page" {
			if _, err := strconv.Atoi(segments[1]); err == nil {
				r.SetPathValue("page", segments[1])
				s.handleHome(w, r)
				return
			}
			s.renderNotFound(w, r, "")
			return
		}
		// /CODE/details → страница плейлиста
		if segments[1] == "details" {
			r.SetPathValue("code", segments[0])
			s.handleDetails(w, r)
			return
		}
		s.renderNotFound(w, r, segments[0])

	default:
		s.renderNotFound(w, r, "")
	}
}

// handleRedirect — перенаправляет на URL плейлиста по коду
func (s *Server) handleRedirect(w http.ResponseWriter, r *http.Request, path string) {
	// strip .m3u / .m3u8 suffix
	code := path
	code = strings.TrimSuffix(code, ".m3u8")
	code = strings.TrimSuffix(code, ".m3u")

	// validate: alphanumeric
	if !isAlphanumeric(code) {
		s.renderNotFound(w, r, code)
		return
	}

	// look up in ini
	ini, err := s.loadIni()
	if err != nil {
		s.renderNotFound(w, r, code)
		return
	}
	pls, exists := ini.Lists[code]
	if !exists || pls.Url == "" {
		s.renderNotFound(w, r, code)
		return
	}

	http.Redirect(w, r, pls.Url, http.StatusFound)
}

// handleAPIGetAll — API: список плейлистов
func (s *Server) handleAPIGetAll(w http.ResponseWriter, r *http.Request) {
	playlists, _, err := s.getPlaylistsView()
	if err != nil {
		s.jsonError(w, http.StatusInternalServerError, err)
		return
	}

	items := make([]map[string]interface{}, 0, len(playlists))
	for _, view := range playlists {
		items = append(items, buildAPIPlaylist(view))
	}

	s.jsonResponse(w, http.StatusOK, map[string]interface{}{
		"items": items,
	})
}

// handleAPIGetOne — API: информация о плейлисте
func (s *Server) handleAPIGetOne(w http.ResponseWriter, r *http.Request) {
	code := r.PathValue("code")
	if code == "" {
		s.jsonError(w, http.StatusBadRequest, fmt.Errorf("code is required"))
		return
	}

	view, ok := s.getPlaylistView(code)
	if !ok {
		s.jsonError(w, http.StatusNotFound, fmt.Errorf("playlist '%s' not found", code))
		return
	}

	s.jsonResponse(w, http.StatusOK, buildAPIPlaylist(*view))
}

// handleAPIGetChannels — API: каналы плейлиста
func (s *Server) handleAPIGetChannels(w http.ResponseWriter, r *http.Request) {
	code := r.PathValue("code")
	if code == "" {
		s.jsonError(w, http.StatusBadRequest, fmt.Errorf("code is required"))
		return
	}

	view, ok := s.getPlaylistView(code)
	if !ok {
		s.jsonError(w, http.StatusNotFound, fmt.Errorf("playlist '%s' not found", code))
		return
	}

	channels := make([]map[string]interface{}, 0, len(view.Channels))
	for _, ch := range view.ViewChannels {
		channels = append(channels, map[string]interface{}{
			"id":          ch.Id,
			"title":       ch.Title,
			"url":         ch.URL,
			"groupId":     ch.GroupId,
			"attributes":  mapToAttributeArray(ch.Attributes),
			"status":      ch.Status,
			"isOnline":    ch.IsOnline,
			"error":       ch.Error,
			"contentType": ch.ContentType,
			"tags":        ch.Tags,
			"checkedAt":   ch.CheckedAt,
			"hasToken":    ch.HasToken,
		})
	}

	s.jsonResponse(w, http.StatusOK, map[string]interface{}{
		"items": channels,
	})
}

// buildAPIPlaylist формирует JSON-представление плейлиста для API
func buildAPIPlaylist(view PlaylistView) map[string]interface{} {
	resp := map[string]interface{}{
		"code":         view.Code,
		"name":         view.Name,
		"description":  view.Description,
		"url":          view.Url,
		"source":       view.Source,
		"isOnline":     nil,
		"onlineCount":  view.OnlineCount,
		"offlineCount": view.OfflineCount,
		"checkedAt":    view.CheckedAt,
		"attributes":   mapToAttributeArray(view.Attributes),
		"groups":       groupsToArray(view.Groups),
		"tags":         view.Tags,
	}

	if view.IsKnown {
		resp["isOnline"] = view.IsOnline
		if view.IsOnline {
			delete(resp, "content")
		} else {
			resp["content"] = view.Content
		}
	}

	return resp
}

// mapToAttributeArray преобразует map атрибутов в массив {name, value}
func mapToAttributeArray(attrs map[string]string) []map[string]string {
	result := make([]map[string]string, 0, len(attrs))
	for name, value := range attrs {
		result = append(result, map[string]string{
			"name":  name,
			"value": value,
		})
	}
	return result
}

// groupsToArray преобразует map групп в массив объектов
func groupsToArray(groups map[string]playlist.Group) []map[string]interface{} {
	result := make([]map[string]interface{}, 0, len(groups))
	for _, g := range groups {
		result = append(result, map[string]interface{}{
			"id":         g.Id,
			"name":       g.Name,
			"attributes": mapToAttributeArray(g.Attributes),
		})
	}
	return result
}

// handleAPIVersion — API: версия
func (s *Server) handleAPIVersion(w http.ResponseWriter, r *http.Request) {
	resp := map[string]interface{}{
		"iptvc": app.VERSION,
	}

	if s.cache != nil {
		ctx := r.Context()
		info, err := s.cache.Info(ctx, "server").Result()
		if err == nil {
			resp["cache"] = parseCacheVersion(info)
		}
	}

	s.jsonResponse(w, http.StatusOK, resp)
}

// handleAPIHealth — API: состояние
func (s *Server) handleAPIHealth(w http.ResponseWriter, r *http.Request) {
	health := map[string]interface{}{
		"timestamp": time.Now().Unix(),
	}

	if s.cache != nil {
		ctx := r.Context()
		health["cache"] = map[string]interface{}{
			"isConnected": s.cache.Ping(ctx).Err() == nil,
		}
	} else {
		health["cache"] = map[string]interface{}{
			"isConnected": false,
		}
	}

	health["iniFile"] = map[string]interface{}{
		"path": s.cfg.App.Playlists,
	}

	s.jsonResponse(w, http.StatusOK, health)
}

// handleAPIStats — API: статистика
func (s *Server) handleAPIStats(w http.ResponseWriter, r *http.Request) {
	playlists, _, err := s.getPlaylistsView()
	if err != nil {
		s.jsonError(w, http.StatusInternalServerError, err)
		return
	}

	stats := computeStats(playlists)
	s.jsonResponse(w, http.StatusOK, stats)
}

// renderNotFound — отображает страницу 404
func (s *Server) renderNotFound(w http.ResponseWriter, r *http.Request, code string) {
	w.WriteHeader(http.StatusNotFound)
	data := &PageData{
		Title:       s.cfg.Site.Title,
		BaseUrl:     s.cfg.Site.BaseUrl,
		RepoUrl:     s.cfg.Site.RepoUrl,
		Version:     app.VERSION,
		Meta:        s.cfg.Site.Meta,
		Menu:        s.cfg.Site.Header.Menu,
		FooterLinks: s.cfg.Site.Footer.Links,
		Tabs:        s.cfg.Site.Tabs,
		Code:        code,
	}
	s.templates.Render(w, "notfound", data)
}

// jsonResponse отправляет JSON-ответ
func (s *Server) jsonResponse(w http.ResponseWriter, status int, data interface{}) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	resp := map[string]interface{}{
		"timestamp": time.Now().Unix(),
	}
	if m, ok := data.(map[string]interface{}); ok {
		for k, v := range m {
			resp[k] = v
		}
	} else {
		resp["data"] = data
	}
	json.NewEncoder(w).Encode(resp)
}

// jsonError отправляет JSON-ошибку
func (s *Server) jsonError(w http.ResponseWriter, status int, err error) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	json.NewEncoder(w).Encode(map[string]interface{}{
		"timestamp": time.Now().Unix(),
		"error": map[string]string{
			"message": err.Error(),
		},
	})
}

// isAlphanumeric проверяет, состоит ли строка только из букв и цифр
func isAlphanumeric(s string) bool {
	for _, c := range s {
		if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')) {
			return false
		}
	}
	return len(s) > 0
}

// parseCacheVersion извлекает версию кеша из INFO-ответа
func parseCacheVersion(info string) string {
	for _, line := range strings.Split(info, "\n") {
		line = strings.TrimSpace(line)
		if strings.HasPrefix(line, "redis_version:") {
			return strings.TrimPrefix(line, "redis_version:")
		}
	}
	return "unknown"
}

// computeStats вычисляет статистику по плейлистам и каналам
func computeStats(playlists []PlaylistView) map[string]interface{} {
	plsOnline := 0
	plsOffline := 0
	plsUnknown := 0
	plsAdult := 0
	plsCatchup := 0
	plsTvg := 0
	plsGroupped := 0

	channelsAll := 0
	channelsOnline := 0
	channelsOffline := 0
	channelsAdult := 0

	var latestCode string
	var latestTime int64

	for _, pls := range playlists {
		if !pls.IsKnown {
			plsUnknown++
		} else if pls.IsOnline {
			plsOnline++
		} else {
			plsOffline++
		}

		if containsTag(pls.Tags, "adult") {
			plsAdult++
		}
		if pls.HasCatchup {
			plsCatchup++
		}
		if pls.HasTvg {
			plsTvg++
		}
		if len(pls.Groups) > 0 {
			plsGroupped++
		}

		if pls.CheckedAt > latestTime {
			latestTime = pls.CheckedAt
			latestCode = pls.Code
		}

		for _, ch := range pls.Channels {
			channelsAll++
			if ch.IsOnline {
				channelsOnline++
			} else {
				channelsOffline++
			}
			if containsTag(ch.Tags, "adult") {
				channelsAdult++
			}
		}
	}

	latest := map[string]interface{}{
		"code": latestCode,
		"time": latestTime,
	}
	if latestTime > 0 {
		latest["timeFmt"] = time.Unix(latestTime, 0).Format("15:04:05 02.01.2006")
	}

	return map[string]interface{}{
		"playlists": map[string]interface{}{
			"all":        len(playlists),
			"online":     plsOnline,
			"offline":    plsOffline,
			"unknown":    plsUnknown,
			"adult":      plsAdult,
			"hasCatchup": plsCatchup,
			"hasTvg":     plsTvg,
			"groupped":   plsGroupped,
			"latest":     latest,
		},
		"channels": map[string]interface{}{
			"all":     channelsAll,
			"online":  channelsOnline,
			"offline": channelsOffline,
			"adult":   channelsAdult,
		},
	}
}

// containsTag проверяет наличие тега в списке
func containsTag(tags []string, tag string) bool {
	for _, t := range tags {
		if t == tag {
			return true
		}
	}
	return false
}
