package service

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"strings"
	"time"
)

type tokenPayload struct {
	ID        string     `json:"id"`
	Name      string     `json:"name"`
	Login     string     `json:"login"`
	Roles     []RoleCode `json:"roles"`
	ExpiresAt int64      `json:"exp"`
}

type apiError struct {
	Error struct {
		Code    string `json:"code"`
		Message string `json:"message"`
	} `json:"error"`
}

func normalizeContentType(value string) ContentType {
	switch strings.ToLower(strings.TrimSpace(value)) {
	case "research":
		return ContentTypeArticle
	case "announcement":
		return ContentTypeNews
	default:
		return ContentType(strings.TrimSpace(value))
	}
}

func normalizeStatus(value string) ContentStatus {
	switch strings.ToLower(strings.TrimSpace(value)) {
	case "draft", "черновик":
		return ContentStatusDraft
	case "moderation", "pending", "на модерации":
		return ContentStatusModeration
	case "review", "на проверке":
		return ContentStatusReview
	case "published", "опубликовано":
		return ContentStatusPublished
	case "returned", "возвращен":
		return ContentStatusReturned
	case "archived", "архив":
		return ContentStatusArchived
	default:
		return ContentStatus(strings.TrimSpace(value))
	}
}

func routeAPI(w http.ResponseWriter, r *http.Request, cfg Config) bool {
	path := strings.TrimPrefix(r.URL.Path, "/api")
	if path == "" {
		path = "/"
	}

	switch cfg.Domain {
	case "auth":
		return handleAuth(w, r, path)
	case "user":
		return handleUser(w, r, path)
	case "content":
		return handleContent(w, r, path)
	case "taxonomy":
		return handleTaxonomy(w, r, path)
	case "speaker":
		return handleSpeaker(w, r, path)
	case "subscription":
		return handleSubscription(w, r, path)
	case "notification":
		return handleNotification(w, r, path)
	case "comment":
		return handleComment(w, r, path)
	case "search":
		return handleSearch(w, r, path)
	case "analytics":
		return handleAnalytics(w, r, path)
	case "audit":
		return handleAudit(w, r, path)
	case "media":
		return handleMedia(w, r, path)
	default:
		return false
	}
}

func handleAuth(w http.ResponseWriter, r *http.Request, path string) bool {
	switch {
	case r.Method == http.MethodPost && path == "/auth/login":
		var payload struct {
			Login    string `json:"login"`
			Email    string `json:"email"`
			Password string `json:"password"`
		}
		if !decodeJSON(w, r, &payload) {
			return true
		}
		login := firstNonEmpty(payload.Login, payload.Email)
		if strings.TrimSpace(login) == "" || strings.TrimSpace(payload.Password) == "" {
			writeAPIError(w, http.StatusBadRequest, "VALIDATION_ERROR", "Укажите логин и пароль")
			return true
		}
		user, ok, err := backendStore.UserByLogin(r.Context(), login)
		if err != nil {
			writeStoreError(w, err)
			return true
		}
		if !ok {
			writeAPIError(w, http.StatusUnauthorized, "INVALID_CREDENTIALS", "Неверный логин или пароль")
			return true
		}
		if err := checkPassword(user.PasswordHash, payload.Password); err != nil {
			writeAPIError(w, http.StatusUnauthorized, "INVALID_CREDENTIALS", "Неверный логин или пароль")
			return true
		}
		token, err := makeToken(user)
		if err != nil {
			writeAPIError(w, http.StatusInternalServerError, "TOKEN_ERROR", "Не удалось выпустить токен")
			return true
		}
		writeJSON(w, http.StatusOK, map[string]any{"token": token, "user": user})
		return true
	case r.Method == http.MethodPost && path == "/auth/register":
		var payload struct {
			Login    string `json:"login"`
			Email    string `json:"email"`
			Password string `json:"password"`
			Name     string `json:"name"`
		}
		if !decodeJSON(w, r, &payload) {
			return true
		}
		login := firstNonEmpty(payload.Login, payload.Email)
		if strings.TrimSpace(login) == "" || len(payload.Password) < 8 {
			writeAPIError(w, http.StatusBadRequest, "VALIDATION_ERROR", "Укажите логин и пароль не короче 8 символов")
			return true
		}
		name := strings.TrimSpace(payload.Name)
		if name == "" {
			name = "Демо-пользователь"
		}
		user, err := backendStore.AddUser(r.Context(), login, name, payload.Password)
		if err != nil {
			writeStoreError(w, err)
			return true
		}
		token, err := makeToken(user)
		if err != nil {
			writeAPIError(w, http.StatusInternalServerError, "TOKEN_ERROR", "Не удалось выпустить токен")
			return true
		}
		writeJSON(w, http.StatusCreated, map[string]any{"token": token, "user": user})
		return true
	case r.Method == http.MethodGet && path == "/auth/me":
		user, ok := requireAuth(w, r)
		if !ok {
			return true
		}
		writeJSON(w, http.StatusOK, map[string]any{"user": user})
		return true
	case r.Method == http.MethodPost && path == "/auth/logout":
		writeJSON(w, http.StatusOK, map[string]bool{"ok": true})
		return true
	case r.Method == http.MethodPost && path == "/auth/change-password":
		user, ok := requireAuth(w, r)
		if !ok {
			return true
		}
		var payload struct {
			NextPassword string `json:"nextPassword"`
		}
		if !decodeJSON(w, r, &payload) {
			return true
		}
		if len(payload.NextPassword) < 8 {
			writeAPIError(w, http.StatusBadRequest, "VALIDATION_ERROR", "Новый пароль должен быть не короче 8 символов")
			return true
		}
		if err := backendStore.UpdatePassword(r.Context(), user.ID, payload.NextPassword); err != nil {
			writeStoreError(w, err)
			return true
		}
		writeJSON(w, http.StatusOK, map[string]bool{"ok": true})
		return true
	default:
		return false
	}
}

func handleUser(w http.ResponseWriter, r *http.Request, path string) bool {
	switch {
	case r.Method == http.MethodGet && (path == "/users" || path == "/admin/users"):
		if _, ok := requireRole(w, r, RoleAdministrator); !ok {
			return true
		}
		items, err := backendStore.ListUsers(r.Context())
		if err != nil {
			writeStoreError(w, err)
			return true
		}
		writeJSON(w, http.StatusOK, map[string]any{"items": items})
		return true
	case r.Method == http.MethodGet && (path == "/roles" || path == "/admin/roles"):
		if _, ok := requireRole(w, r, RoleAdministrator); !ok {
			return true
		}
		writeJSON(w, http.StatusOK, map[string]any{"items": []map[string]any{
			{"code": RoleAdministrator, "permissions": []string{"*"}},
			{"code": RoleEditor, "permissions": []string{"content:create", "content:update", "comments:moderate"}},
			{"code": RoleManager, "permissions": []string{"content:create", "content:publish", "subscriptions:manage"}},
			{"code": RoleUser, "permissions": []string{"content:read", "comments:create", "subscriptions:create"}},
		}})
		return true
	default:
		return false
	}
}

func handleContent(w http.ResponseWriter, r *http.Request, path string) bool {
	switch {
	case r.Method == http.MethodGet && path == "/content":
		query := r.URL.Query()
		user, _ := userFromRequest(r)
		items, err := backendStore.ListContent(r.Context(), ContentFilter{
			Term:        query.Get("q"),
			Category:    query.Get("category"),
			ContentType: string(normalizeContentType(query.Get("type"))),
			SortMode:    query.Get("sort"),
			Status:      string(normalizeStatus(query.Get("status"))),
			AuthorID:    query.Get("authorId"),
			Mine:        query.Get("mine") == "true",
			User:        user,
			Exclude:     query.Get("exclude"),
			Limit: func() int {
				if query.Get("limit") == "" {
					return 0
				}
				var n int
				fmt.Sscanf(query.Get("limit"), "%d", &n)
				return n
			}(),
		})
		if err != nil {
			writeStoreError(w, err)
			return true
		}
		writeJSON(w, http.StatusOK, map[string]any{"items": items})
		return true
	case r.Method == http.MethodGet && path == "/events":
		items, err := backendStore.ListContent(r.Context(), ContentFilter{OnlyEvents: true, SortMode: "newest"})
		if err != nil {
			writeStoreError(w, err)
			return true
		}
		writeJSON(w, http.StatusOK, map[string]any{"items": items, "note": "Event представлен как тип контента до подтверждения отдельной сущности."})
		return true
	case r.Method == http.MethodGet && strings.HasPrefix(path, "/content/"):
		id := strings.TrimPrefix(path, "/content/")
		item, ok, err := backendStore.ContentByID(r.Context(), id)
		if err != nil {
			writeStoreError(w, err)
			return true
		}
		if !ok {
			writeAPIError(w, http.StatusNotFound, "NOT_FOUND", "Материал не найден")
			return true
		}
		writeJSON(w, http.StatusOK, map[string]any{"item": item})
		return true
	case r.Method == http.MethodPost && path == "/content":
		user, ok := requireAnyRole(w, r, RoleAdministrator, RoleEditor, RoleManager)
		if !ok {
			return true
		}
		var payload ContentItem
		if !decodeJSON(w, r, &payload) {
			return true
		}
		payload.Type = normalizeContentType(string(payload.Type))
		payload.Status = normalizeStatus(string(payload.Status))
		payload.Lead = firstNonEmpty(payload.Lead, payload.Excerpt)
		payload.Body = firstNonEmpty(payload.Body, payload.Content)
		if strings.TrimSpace(payload.Title) == "" || payload.Type == "" || strings.TrimSpace(payload.Category) == "" {
			writeAPIError(w, http.StatusBadRequest, "VALIDATION_ERROR", "Укажите название, категорию и тип материала")
			return true
		}
		item := ContentItem{
			ID:               fmt.Sprintf("demo-content-%d", time.Now().UnixNano()),
			Title:            payload.Title,
			Lead:             firstNonEmpty(payload.Lead, "Демонстрационный черновик"),
			Body:             firstNonEmpty(payload.Body, "Демо-описание материала."),
			Type:             payload.Type,
			Category:         payload.Category,
			Tags:             payload.Tags,
			Author:           user.Name,
			PublishedAt:      time.Now().Format("2006-01-02"),
			Visibility:       VisibilityAuthenticated,
			Status:           firstStatus(payload.Status, ContentStatusDraft),
			ImageTone:        firstNonEmpty(payload.ImageTone, "from-university-800 via-slate-700 to-sky-300"),
			MediaURL:         payload.MediaURL,
			MediaKind:        payload.MediaKind,
			MimeType:         payload.MimeType,
			FileName:         payload.FileName,
			FileSize:         payload.FileSize,
			ModeratorComment: payload.ModeratorComment,
			ReviewComment:    payload.ReviewComment,
		}
		stored, err := backendStore.AddContent(r.Context(), item)
		if err != nil {
			writeStoreError(w, err)
			return true
		}
		writeJSON(w, http.StatusCreated, map[string]any{"item": stored})
		return true
	case r.Method == http.MethodPatch && strings.HasPrefix(path, "/content/"):
		if _, ok := requireAnyRole(w, r, RoleAdministrator, RoleEditor, RoleManager); !ok {
			return true
		}
		var payload ContentItem
		if !decodeJSON(w, r, &payload) {
			return true
		}
		payload.Type = normalizeContentType(string(payload.Type))
		payload.Status = normalizeStatus(string(payload.Status))
		payload.Lead = firstNonEmpty(payload.Lead, payload.Excerpt)
		payload.Body = firstNonEmpty(payload.Body, payload.Content)
		item, ok, err := backendStore.PatchContent(r.Context(), strings.TrimPrefix(path, "/content/"), payload)
		if err != nil {
			writeStoreError(w, err)
			return true
		}
		if !ok {
			writeAPIError(w, http.StatusNotFound, "NOT_FOUND", "Материал не найден")
			return true
		}
		writeJSON(w, http.StatusOK, map[string]any{"item": item})
		return true
	case r.Method == http.MethodDelete && strings.HasPrefix(path, "/content/"):
		if _, ok := requireRole(w, r, RoleAdministrator); !ok {
			return true
		}
		deleted, err := backendStore.DeleteContent(r.Context(), strings.TrimPrefix(path, "/content/"))
		if err != nil {
			writeStoreError(w, err)
			return true
		}
		if !deleted {
			writeAPIError(w, http.StatusNotFound, "NOT_FOUND", "Материал не найден")
			return true
		}
		writeJSON(w, http.StatusOK, map[string]bool{"ok": true})
		return true
	default:
		return false
	}
}

func handleTaxonomy(w http.ResponseWriter, r *http.Request, path string) bool {
	if r.Method != http.MethodGet {
		return false
	}
	switch path {
	case "/categories":
		items, err := backendStore.ListCategories(r.Context())
		if err != nil {
			writeStoreError(w, err)
			return true
		}
		writeJSON(w, http.StatusOK, map[string]any{"items": items})
		return true
	case "/tags":
		items, err := backendStore.ListTags(r.Context())
		if err != nil {
			writeStoreError(w, err)
			return true
		}
		writeJSON(w, http.StatusOK, map[string]any{"items": items})
		return true
	default:
		return false
	}
}

func handleSpeaker(w http.ResponseWriter, r *http.Request, path string) bool {
	if r.Method == http.MethodGet && path == "/speakers" {
		items, err := backendStore.ListSpeakers(r.Context())
		if err != nil {
			writeStoreError(w, err)
			return true
		}
		writeJSON(w, http.StatusOK, map[string]any{"items": items})
		return true
	}
	return false
}

func handleMedia(w http.ResponseWriter, r *http.Request, path string) bool {
	switch {
	case r.Method == http.MethodGet && path == "/media":
		items, err := backendStore.ListContent(r.Context(), ContentFilter{OnlyMedia: true, SortMode: "newest"})
		if err != nil {
			writeStoreError(w, err)
			return true
		}
		writeJSON(w, http.StatusOK, map[string]any{"items": items})
		return true
	case r.Method == http.MethodGet && strings.HasPrefix(path, "/media/files/"):
		file, ok, err := backendStore.FileByID(r.Context(), strings.TrimPrefix(path, "/media/files/"))
		if err != nil {
			writeStoreError(w, err)
			return true
		}
		if !ok {
			writeAPIError(w, http.StatusNotFound, "NOT_FOUND", "Файл не найден")
			return true
		}
		w.Header().Set("Content-Type", firstNonEmpty(file.MimeType, "application/octet-stream"))
		w.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=%q", file.Name))
		http.ServeContent(w, r, file.Name, time.Now(), bytes.NewReader(file.Data))
		return true
	case r.Method == http.MethodPost && path == "/media":
		user, ok := requireAnyRole(w, r, RoleAdministrator, RoleEditor, RoleManager)
		if !ok {
			return true
		}

		r.Body = http.MaxBytesReader(w, r.Body, 64<<20)
		if err := r.ParseMultipartForm(64 << 20); err != nil {
			writeAPIError(w, http.StatusBadRequest, "INVALID_MULTIPART", "Не удалось прочитать multipart-запрос")
			return true
		}

		uploaded, header, err := r.FormFile("file")
		if err != nil {
			writeAPIError(w, http.StatusBadRequest, "VALIDATION_ERROR", "Прикрепите файл")
			return true
		}
		defer uploaded.Close()

		data, err := io.ReadAll(uploaded)
		if err != nil || len(data) == 0 {
			writeAPIError(w, http.StatusBadRequest, "INVALID_FILE", "Файл пустой или поврежден")
			return true
		}

		mimeType := header.Header.Get("Content-Type")
		if mimeType == "" || mimeType == "application/octet-stream" {
			sample := data
			if len(sample) > 512 {
				sample = sample[:512]
			}
			mimeType = http.DetectContentType(sample)
		}

		mediaKind := inferMediaKind(mimeType, header.Filename)
		contentType := ContentType(strings.TrimSpace(r.FormValue("type")))
		if contentType == "" {
			contentType = defaultContentTypeForMediaKind(mediaKind)
		}
		if !isValidContentType(contentType) {
			writeAPIError(w, http.StatusBadRequest, "VALIDATION_ERROR", "Укажите корректный тип материала")
			return true
		}

		stored, err := backendStore.AddFile(r.Context(), StoredFile{
			ID:       fmt.Sprintf("demo-file-%d", time.Now().UnixNano()),
			Name:     firstNonEmpty(header.Filename, "uploaded-file"),
			MimeType: mimeType,
			Size:     int64(len(data)),
			Data:     data,
		})
		if err != nil {
			writeStoreError(w, err)
			return true
		}

		category := strings.TrimSpace(r.FormValue("category"))
		if category == "" {
			category = defaultCategoryForMediaKind(mediaKind)
		}
		item := ContentItem{
			ID:          fmt.Sprintf("demo-media-%d", time.Now().UnixNano()),
			Title:       firstNonEmpty(r.FormValue("title"), stored.Name),
			Lead:        firstNonEmpty(r.FormValue("lead"), "Демонстрационный медиаматериал с загруженным файлом."),
			Body:        firstNonEmpty(r.FormValue("body"), "Файл загружен в in-memory demo-хранилище и доступен только до перезапуска сервиса."),
			Type:        contentType,
			Category:    category,
			Tags:        parseTags(r.FormValue("tags")),
			Author:      user.Name,
			PublishedAt: time.Now().Format("2006-01-02"),
			Visibility:  VisibilityAuthenticated,
			Status:      ContentStatusDraft,
			Views:       0,
			ImageTone:   "from-university-800 via-slate-700 to-sky-300",
			MediaURL:    "/api/media/files/" + stored.ID,
			MediaKind:   mediaKind,
			MimeType:    stored.MimeType,
			FileName:    stored.Name,
			FileSize:    stored.Size,
		}
		created, err := backendStore.AddContent(r.Context(), item)
		if err != nil {
			writeStoreError(w, err)
			return true
		}
		writeJSON(w, http.StatusCreated, map[string]any{"item": created})
		return true
	}
	return false
}

func handleSearch(w http.ResponseWriter, r *http.Request, path string) bool {
	if r.Method == http.MethodGet && path == "/search" {
		query := r.URL.Query()
		items, err := backendStore.ListContent(r.Context(), ContentFilter{Term: query.Get("q"), Category: query.Get("category"), ContentType: query.Get("type"), SortMode: query.Get("sort")})
		if err != nil {
			writeStoreError(w, err)
			return true
		}
		writeJSON(w, http.StatusOK, map[string]any{"items": items})
		return true
	}
	return false
}

func handleSubscription(w http.ResponseWriter, r *http.Request, path string) bool {
	if path != "/subscriptions" {
		return false
	}
	user, ok := requireAuth(w, r)
	if !ok {
		return true
	}
	switch r.Method {
	case http.MethodGet:
		writeJSON(w, http.StatusOK, map[string]any{"items": user.Subscriptions})
		return true
	case http.MethodPost:
		var payload struct {
			Target string `json:"target"`
		}
		if !decodeJSON(w, r, &payload) {
			return true
		}
		if strings.TrimSpace(payload.Target) == "" {
			writeAPIError(w, http.StatusBadRequest, "VALIDATION_ERROR", "Укажите объект подписки")
			return true
		}
		items, err := backendStore.UpsertSubscription(r.Context(), user.ID, payload.Target)
		if err != nil {
			writeStoreError(w, err)
			return true
		}
		writeJSON(w, http.StatusOK, map[string]any{"items": items})
		return true
	default:
		return false
	}
}

func handleNotification(w http.ResponseWriter, r *http.Request, path string) bool {
	switch {
	case r.Method == http.MethodGet && path == "/notifications":
		items, err := backendStore.ListNotifications(r.Context())
		if err != nil {
			writeStoreError(w, err)
			return true
		}
		writeJSON(w, http.StatusOK, map[string]any{"items": items})
		return true
	case r.Method == http.MethodPatch && strings.HasPrefix(path, "/notifications/") && strings.HasSuffix(path, "/read"):
		if _, ok := requireAuth(w, r); !ok {
			return true
		}
		id := strings.TrimSuffix(strings.TrimPrefix(path, "/notifications/"), "/read")
		updated, err := backendStore.MarkNotificationRead(r.Context(), id)
		if err != nil {
			writeStoreError(w, err)
			return true
		}
		writeJSON(w, http.StatusOK, map[string]any{"item": updated})
		return true
	default:
		return false
	}
}

func handleComment(w http.ResponseWriter, r *http.Request, path string) bool {
	if !strings.HasPrefix(path, "/comments/") {
		return false
	}
	contentID := strings.TrimPrefix(path, "/comments/")
	switch r.Method {
	case http.MethodGet:
		items, err := backendStore.ListComments(r.Context(), contentID)
		if err != nil {
			writeStoreError(w, err)
			return true
		}
		writeJSON(w, http.StatusOK, map[string]any{"items": items})
		return true
	case http.MethodPost:
		user, ok := requireAuth(w, r)
		if !ok {
			return true
		}
		var payload struct {
			Text string `json:"text"`
		}
		if !decodeJSON(w, r, &payload) {
			return true
		}
		if strings.TrimSpace(payload.Text) == "" {
			writeAPIError(w, http.StatusBadRequest, "VALIDATION_ERROR", "Комментарий не может быть пустым")
			return true
		}
		item, err := backendStore.AddComment(r.Context(), contentID, user, strings.TrimSpace(payload.Text))
		if err != nil {
			writeStoreError(w, err)
			return true
		}
		writeJSON(w, http.StatusCreated, map[string]any{"item": item})
		return true
	default:
		return false
	}
}

func handleAnalytics(w http.ResponseWriter, r *http.Request, path string) bool {
	if r.Method != http.MethodGet || (path != "/analytics/summary" && path != "/admin/dashboard") {
		return false
	}
	user, ok := requireAuth(w, r)
	if !ok {
		return true
	}
	items, err := backendStore.ListContent(r.Context(), ContentFilter{})
	if err != nil {
		writeStoreError(w, err)
		return true
	}
	speakers, err := backendStore.ListSpeakers(r.Context())
	if err != nil {
		writeStoreError(w, err)
		return true
	}
	users, err := backendStore.ListUsers(r.Context())
	if err != nil {
		writeStoreError(w, err)
		return true
	}
	comments, err := backendStore.CountComments(r.Context())
	if err != nil {
		writeStoreError(w, err)
		return true
	}
	totalViews := 0
	moderationQueue := 0
	for _, item := range items {
		totalViews += item.Views
		if item.Status != ContentStatusPublished {
			moderationQueue++
		}
	}
	subscribers := 0
	for _, speaker := range speakers {
		subscribers += speaker.Subscribers
	}
	if path == "/admin/dashboard" {
		writeJSON(w, http.StatusOK, map[string]any{
			"users":     len(users),
			"materials": len(items),
			"views":     totalViews,
			"pending":   moderationQueue,
			"viewsByDay": []map[string]any{
				{"label": "Пн", "value": 120},
				{"label": "Вт", "value": 180},
				{"label": "Ср", "value": 160},
				{"label": "Чт", "value": 220},
				{"label": "Пт", "value": 260},
				{"label": "Сб", "value": 140},
				{"label": "Вс", "value": 110},
			},
			"roles": []RoleCode{RoleAdministrator, RoleEditor, RoleManager, RoleUser},
		})
		return true
	}
	popular := append([]ContentItem(nil), items...)
	materials := 0
	for _, item := range items {
		if item.Author == user.Name {
			materials++
		}
	}
	writeJSON(w, http.StatusOK, map[string]any{"materials": materials, "comments": comments, "totalViews": totalViews, "subscribers": subscribers, "activeUsers": len(users), "popular": popular})
	return true
}

func handleAudit(w http.ResponseWriter, r *http.Request, path string) bool {
	if r.Method == http.MethodGet && (path == "/admin/audit" || path == "/audit") {
		if _, ok := requireRole(w, r, RoleAdministrator); !ok {
			return true
		}
		items, err := backendStore.ListAudit(r.Context())
		if err != nil {
			writeStoreError(w, err)
			return true
		}
		writeJSON(w, http.StatusOK, map[string]any{"items": items})
		return true
	}
	return false
}

func decodeJSON(w http.ResponseWriter, r *http.Request, target any) bool {
	defer r.Body.Close()
	decoder := json.NewDecoder(r.Body)
	decoder.DisallowUnknownFields()
	if err := decoder.Decode(target); err != nil {
		writeAPIError(w, http.StatusBadRequest, "INVALID_JSON", "Некорректный JSON-запрос")
		return false
	}
	return true
}

func writeAPIError(w http.ResponseWriter, status int, code, message string) {
	payload := apiError{}
	payload.Error.Code = code
	payload.Error.Message = message
	writeJSON(w, status, payload)
}

func writeStoreError(w http.ResponseWriter, err error) {
	writeAPIError(w, http.StatusServiceUnavailable, "STORE_UNAVAILABLE", err.Error())
}

func makeToken(user UserProfile) (string, error) {
	payload := tokenPayload{ID: user.ID, Name: user.Name, Login: user.Login, Roles: user.Roles, ExpiresAt: time.Now().UTC().Add(tokenTTL()).Unix()}
	return signToken(payload)
}

func userFromRequest(r *http.Request) (UserProfile, bool) {
	header := r.Header.Get("Authorization")
	if !strings.HasPrefix(header, "Bearer ") {
		return UserProfile{}, false
	}
	token := strings.TrimPrefix(header, "Bearer ")
	payload, err := parseToken(token)
	if err != nil {
		return UserProfile{}, false
	}
	user, ok, err := backendStore.UserByID(r.Context(), payload.ID)
	if err != nil {
		return UserProfile{}, false
	}
	if !ok {
		return UserProfile{}, false
	}
	return user, true
}

func requireAuth(w http.ResponseWriter, r *http.Request) (UserProfile, bool) {
	user, ok := userFromRequest(r)
	if !ok {
		writeAPIError(w, http.StatusUnauthorized, "UNAUTHORIZED", "Требуется аутентификация")
		return UserProfile{}, false
	}
	return user, true
}

func requireRole(w http.ResponseWriter, r *http.Request, role RoleCode) (UserProfile, bool) {
	return requireAnyRole(w, r, role)
}

func requireAnyRole(w http.ResponseWriter, r *http.Request, roles ...RoleCode) (UserProfile, bool) {
	user, ok := requireAuth(w, r)
	if !ok {
		return UserProfile{}, false
	}
	for _, actual := range user.Roles {
		for _, expected := range roles {
			if actual == expected {
				return user, true
			}
		}
	}
	writeAPIError(w, http.StatusForbidden, "FORBIDDEN", "Недостаточно прав доступа")
	return UserProfile{}, false
}

func firstNonEmpty(value, fallback string) string {
	if strings.TrimSpace(value) == "" {
		return fallback
	}
	return value
}

func firstStatus(value, fallback ContentStatus) ContentStatus {
	if strings.TrimSpace(string(value)) == "" {
		return fallback
	}
	return value
}

func inferMediaKind(mimeType, fileName string) string {
	lowerName := strings.ToLower(fileName)
	switch {
	case strings.HasPrefix(mimeType, "image/"):
		return "image"
	case strings.HasPrefix(mimeType, "video/"):
		return "video"
	case strings.HasPrefix(mimeType, "audio/"):
		return "audio"
	case mimeType == "application/pdf" || strings.HasSuffix(lowerName, ".pdf") || strings.HasSuffix(lowerName, ".doc") || strings.HasSuffix(lowerName, ".docx") || strings.HasSuffix(lowerName, ".ppt") || strings.HasSuffix(lowerName, ".pptx") || strings.HasSuffix(lowerName, ".xls") || strings.HasSuffix(lowerName, ".xlsx") || strings.HasSuffix(lowerName, ".txt") || strings.HasSuffix(lowerName, ".rtf"):
		return "document"
	default:
		return "other"
	}
}

func defaultContentTypeForMediaKind(kind string) ContentType {
	switch kind {
	case "video":
		return ContentTypeVideo
	case "audio":
		return ContentTypeAudio
	case "image":
		return ContentTypeGraphic
	default:
		return ContentTypeArticle
	}
}

func defaultCategoryForMediaKind(kind string) string {
	switch kind {
	case "video":
		return "Видео"
	case "audio":
		return "Аудио"
	case "image":
		return "Графика"
	default:
		return "Статьи"
	}
}

func isValidContentType(contentType ContentType) bool {
	switch contentType {
	case ContentTypeNews, ContentTypeArticle, ContentTypeVideo, ContentTypeAudio, ContentTypeGraphic, ContentTypeEvent:
		return true
	default:
		return false
	}
}

func parseTags(value string) []string {
	parts := strings.FieldsFunc(value, func(r rune) bool { return r == ',' || r == ';' || r == '\n' })
	tags := make([]string, 0, len(parts))
	seen := map[string]bool{}
	for _, part := range parts {
		tag := strings.TrimSpace(part)
		if tag == "" || seen[tag] {
			continue
		}
		seen[tag] = true
		tags = append(tags, tag)
	}
	if len(tags) == 0 {
		return []string{"демо", "файл"}
	}
	return tags
}
