package controllers

import (
	database "ares/database/config"
	"ares/database/models"
	"encoding/json"
	"errors"
	"fmt"
	"mime/multipart"
	"net/http"
	"path/filepath"
	"regexp"
	"strconv"
	"strings"
	"time"

	"github.com/gofiber/fiber/v3"
	"gorm.io/gorm"
)

// reuse package-level `validate` defined in other controller files
// (one global validator instance is sufficient)

func parseIDsCSV(s string) []uint {
	if s == "" {
		return nil
	}
	parts := strings.Split(s, ",")
	out := make([]uint, 0, len(parts))
	for _, p := range parts {
		p = strings.TrimSpace(p)
		if p == "" {
			continue
		}
		v, err := strconv.ParseUint(p, 10, 64)
		if err == nil {
			out = append(out, uint(v))
		}
	}
	return out
}

// slugify converts a title to a URL-friendly slug
func slugify(s string) string {
	// replace common Turkish characters with ASCII equivalents
	replacer := strings.NewReplacer(
		"Ç", "C", "ç", "c",
		"Ğ", "G", "ğ", "g",
		"İ", "I", "ı", "i",
		"Ö", "O", "ö", "o",
		"Ş", "S", "ş", "s",
		"Ü", "U", "ü", "u",
	)
	s = replacer.Replace(s)
	s = strings.ToLower(s)
	re := regexp.MustCompile("[^a-z0-9]+")
	s = re.ReplaceAllString(s, "-")
	s = strings.Trim(s, "-")
	return s
}

// UpdateCategoryRequest represents payload for updating a category
// swagger:model UpdateCategoryRequest
type UpdateCategoryRequest struct {
	Title       *string `json:"title" validate:"omitempty,min=2"`
	Description *string `json:"description"`
	ParentID    *uint   `json:"parent_id"`
}

// makeUniqueSlug ensures the slug is unique in posts table, appending suffix if needed
func makeUniqueSlug(base string) string {
	slug := base
	var count int64
	i := 1
	for {
		database.DB.Model(&models.Post{}).Where("slug = ?", slug).Count(&count)
		if count == 0 {
			return slug
		}
		slug = base + "-" + strconv.Itoa(i)
		i++
	}
}

// makeUniqueSlugExclude ensures uniqueness excluding a specific post id (used on updates)
func makeUniqueSlugExclude(base string, excludeID uint64) string {
	slug := base
	var count int64
	i := 1
	for {
		database.DB.Model(&models.Post{}).Where("slug = ? AND id != ?", slug, excludeID).Count(&count)
		if count == 0 {
			return slug
		}
		slug = base + "-" + strconv.Itoa(i)
		i++
	}
}

// makeUniqueSlugCategory ensures the slug is unique in categories table
func makeUniqueSlugCategory(base string) string {
	slug := base
	var count int64
	i := 1
	for {
		database.DB.Model(&models.Category{}).Where("slug = ?", slug).Count(&count)
		if count == 0 {
			return slug
		}
		slug = base + "-" + strconv.Itoa(i)
		i++
	}
}

// makeUniqueSlugCategoryExclude ensures the slug is unique in categories table excluding an id
func makeUniqueSlugCategoryExclude(base string, excludeID uint64) string {
	slug := base
	var count int64
	i := 1
	for {
		database.DB.Model(&models.Category{}).Where("slug = ? AND id != ?", slug, excludeID).Count(&count)
		if count == 0 {
			return slug
		}
		slug = base + "-" + strconv.Itoa(i)
		i++
	}
}

func saveUploadedFiles(c fiber.Ctx, files []*multipart.FileHeader) ([]string, error) {
	if len(files) == 0 {
		return nil, nil
	}
	saved := make([]string, 0, len(files))
	for _, fh := range files {
		filename := fmt.Sprintf("%d_%s", time.Now().UnixNano(), fh.Filename)
		path := filepath.Join("./uploads/posts", filename)
		if err := c.SaveFile(fh, path); err != nil {
			return nil, err
		}
		saved = append(saved, "/uploads/posts/"+filename)
	}
	return saved, nil
}

// -------- POSTS --------

// GetPosts godoc
// @Summary List posts (public) with pagination
// @Tags Posts
// @Produce json
// @Param page query int false "Page number"
// @Param per_page query int false "Items per page"
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/posts [get]
func GetPosts(c fiber.Ctx) error {
	pageStr := c.Query("page", "1")
	perPageStr := c.Query("per_page", "10")
	page, _ := strconv.Atoi(pageStr)
	perPage, _ := strconv.Atoi(perPageStr)
	if page < 1 {
		page = 1
	}
	if perPage < 1 {
		perPage = 10
	}
	if perPage > 100 {
		perPage = 100
	}
	offset := (page - 1) * perPage

	var total int64
	database.DB.Model(&models.Post{}).Count(&total)

	var posts []models.Post
	database.DB.Preload("Categories").Preload("Tags").Limit(perPage).Offset(offset).Find(&posts)

	return c.JSON(fiber.Map{
		"data": posts,
		"meta": fiber.Map{"page": page, "per_page": perPage, "total": total},
	})
}

// GetPost godoc
// @Summary Get single post (public)
// @Tags Posts
// @Produce json
// @Param slug path string true "Post slug"
// @Success 200 {object} models.PostDoc
// @Failure 404 {object} map[string]string
// @Router /api/v1/posts/{slug} [get]
func GetPost(c fiber.Ctx) error {
	slug := c.Params("slug")
	if slug == "" {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid slug"})
	}
	var post models.Post
	if err := database.DB.Preload("Categories").Preload("Tags").Where("slug = ? AND deleted_at IS NULL", slug).First(&post).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "post not found"})
		}
		return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "db error"})
	}
	return c.JSON(post)
}

func CreatePost(c fiber.Ctx) error {
	// Support both multipart/form-data and JSON
	if strings.HasPrefix(c.Get("Content-Type"), "multipart/form-data") {
		form, err := c.MultipartForm()
		if err != nil {
			return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid multipart form"})
		}
		title := firstValue(form.Value, "title")
		if title == "" {
			return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "title required"})
		}
		content := firstValue(form.Value, "content")
		catIDs := parseIDsCSV(firstValue(form.Value, "category_ids"))
		tagIDs := parseIDsCSV(firstValue(form.Value, "tag_ids"))
		files := form.File["images"]
		saved, err := saveUploadedFiles(c, files)
		if err != nil {
			return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "failed to save images"})
		}
		imgsJSON, _ := json.Marshal(saved)
		baseSlug := slugify(title)
		post := models.Post{Title: title, Content: content, Images: string(imgsJSON)}
		post.Slug = makeUniqueSlug(baseSlug)
		if len(catIDs) > 0 {
			var cats []models.Category
			database.DB.Find(&cats, catIDs)
			post.Categories = cats
		}
		if len(tagIDs) > 0 {
			var tags []models.Tag
			database.DB.Find(&tags, tagIDs)
			post.Tags = tags
		}
		if err := database.DB.Create(&post).Error; err != nil {
			return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "could not create post"})
		}
		return c.Status(http.StatusCreated).JSON(post)
	}

	// JSON fallback
	var input struct {
		Title       string   `json:"title" validate:"required,min=3"`
		Content     string   `json:"content"`
		CategoryIDs []uint   `json:"category_ids"`
		TagIDs      []uint   `json:"tag_ids"`
		Images      []string `json:"images"`
	}
	if err := c.Bind().Body(&input); err != nil {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid body"})
	}
	if err := validate.Struct(input); err != nil {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
	}
	imgsJSON, _ := json.Marshal(input.Images)
	baseSlug := slugify(input.Title)
	post := models.Post{Title: input.Title, Content: input.Content, Images: string(imgsJSON)}
	post.Slug = makeUniqueSlug(baseSlug)
	if len(input.CategoryIDs) > 0 {
		var cats []models.Category
		database.DB.Find(&cats, input.CategoryIDs)
		post.Categories = cats
	}
	if len(input.TagIDs) > 0 {
		var tags []models.Tag
		database.DB.Find(&tags, input.TagIDs)
		post.Tags = tags
	}
	if err := database.DB.Create(&post).Error; err != nil {
		return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "could not create post"})
	}
	return c.Status(http.StatusCreated).JSON(post)
}

func firstValue(m map[string][]string, key string) string {
	if m == nil {
		return ""
	}
	if v, ok := m[key]; ok && len(v) > 0 {
		return v[0]
	}
	return ""
}


func UpdatePost(c fiber.Ctx) error {
	// multipart or JSON handling similar to CreatePost
	id, err := strconv.ParseUint(c.Params("id"), 10, 64)
	if err != nil {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid id"})
	}
	var post models.Post
	if err := database.DB.Preload("Categories").Preload("Tags").First(&post, id).Error; err != nil {
		return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "post not found"})
	}

	if strings.HasPrefix(c.Get("Content-Type"), "multipart/form-data") {
		form, err := c.MultipartForm()
		if err != nil {
			return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid multipart form"})
		}
		title := firstValue(form.Value, "title")
		if title != "" {
			post.Title = title
			// regenerate slug on title change
			post.Slug = makeUniqueSlugExclude(slugify(title), uint64(post.ID))
		}
		content := firstValue(form.Value, "content")
		if content != "" {
			post.Content = content
		}
		catIDs := parseIDsCSV(firstValue(form.Value, "category_ids"))
		if len(catIDs) > 0 {
			var cats []models.Category
			database.DB.Find(&cats, catIDs)
			if err := database.DB.Model(&post).Association("Categories").Replace(&cats); err != nil {
				return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "could not update categories"})
			}
		}
		tagIDs := parseIDsCSV(firstValue(form.Value, "tag_ids"))
		if len(tagIDs) > 0 {
			var tags []models.Tag
			database.DB.Find(&tags, tagIDs)
			if err := database.DB.Model(&post).Association("Tags").Replace(&tags); err != nil {
				return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "could not update tags"})
			}
		}
		files := form.File["images"]
		if len(files) > 0 {
			saved, err := saveUploadedFiles(c, files)
			if err != nil {
				return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "failed to save images"})
			}
			imgsJSON, _ := json.Marshal(saved)
			post.Images = string(imgsJSON)
		}
		if err := database.DB.Save(&post).Error; err != nil {
			return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "could not update post"})
		}
		return c.JSON(post)
	}

	// JSON fallback
	var input struct {
		Title       *string  `json:"title" validate:"omitempty,min=3"`
		Content     *string  `json:"content"`
		CategoryIDs []uint   `json:"category_ids"`
		TagIDs      []uint   `json:"tag_ids"`
		Images      []string `json:"images"`
	}
	if err := c.Bind().Body(&input); err != nil {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid body"})
	}
	if err := validate.Struct(input); err != nil {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
	}
	if input.Title != nil {
		post.Title = *input.Title
		post.Slug = makeUniqueSlugExclude(slugify(*input.Title), uint64(post.ID))
	}
	if input.Content != nil {
		post.Content = *input.Content
	}
	if input.CategoryIDs != nil {
		var cats []models.Category
		database.DB.Find(&cats, input.CategoryIDs)
		if err := database.DB.Model(&post).Association("Categories").Replace(&cats); err != nil {
			return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "could not update categories"})
		}
	}
	if input.TagIDs != nil {
		var tags []models.Tag
		database.DB.Find(&tags, input.TagIDs)
		if err := database.DB.Model(&post).Association("Tags").Replace(&tags); err != nil {
			return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "could not update tags"})
		}
	}
	if input.Images != nil {
		imgsJSON, _ := json.Marshal(input.Images)
		post.Images = string(imgsJSON)
	}
	if err := database.DB.Save(&post).Error; err != nil {
		return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "could not update post"})
	}
	return c.JSON(post)
}


func DeletePost(c fiber.Ctx) error {
	id, err := strconv.ParseUint(c.Params("id"), 10, 64)
	if err != nil {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid id"})
	}
	var post models.Post
	if err := database.DB.First(&post, id).Error; err != nil {
		return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "post not found"})
	}
	if err := database.DB.Delete(&post).Error; err != nil {
		return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "could not delete post"})
	}
	return c.JSON(fiber.Map{"message": "post deleted"})
}

func AdminListPosts(c fiber.Ctx) error {
	trashed := c.Query("trashed", "none")
	if trashed != "none" && trashed != "only" && trashed != "with" {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid trashed parameter, must be one of: none, only, with"})
	}

	pageStr := c.Query("page", "1")
	perPageStr := c.Query("per_page", "10")
	page, _ := strconv.Atoi(pageStr)
	perPage, _ := strconv.Atoi(perPageStr)
	if page < 1 {
		page = 1
	}
	if perPage < 1 {
		perPage = 10
	}
	if perPage > 100 {
		perPage = 100
	}
	offset := (page - 1) * perPage

	var total int64
	var posts []models.Post
	db := database.DB

	switch trashed {
	case "none":
		db.Model(&models.Post{}).Count(&total)
		db.Preload("Categories").Preload("Tags").Order("id desc").Limit(perPage).Offset(offset).Find(&posts)
	case "only":
		db.Unscoped().Model(&models.Post{}).Where("deleted_at IS NOT NULL").Count(&total)
		db.Unscoped().Preload("Categories").Preload("Tags").Where("deleted_at IS NOT NULL").Order("deleted_at desc").Limit(perPage).Offset(offset).Find(&posts)
	case "with":
		db.Unscoped().Model(&models.Post{}).Count(&total)
		db.Unscoped().Preload("Categories").Preload("Tags").Order("id desc").Limit(perPage).Offset(offset).Find(&posts)
	}

	return c.JSON(fiber.Map{"data": posts, "meta": fiber.Map{"page": page, "per_page": perPage, "total": total}})
}

func HardDeletePost(c fiber.Ctx) error {
	if database.DB == nil {
		return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "database is not configured"})
	}

	id, err := strconv.ParseUint(c.Params("id"), 10, 64)
	if err != nil || id == 0 {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid post id"})
	}

	var post models.Post
	if err := database.DB.Unscoped().First(&post, id).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "post not found"})
		}
		return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"})
	}

	err = database.DB.Transaction(func(tx *gorm.DB) error {
		// clear many2many associations to avoid orphaned join rows
		if err := tx.Model(&post).Association("Categories").Clear(); err != nil {
			return err
		}
		if err := tx.Model(&post).Association("Tags").Clear(); err != nil {
			return err
		}
		// permanently delete the post
		return tx.Unscoped().Delete(&post).Error
	})
	if err != nil {
		return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "post hard-delete failed"})
	}

	return c.JSON(fiber.Map{
		"message": "post permanently deleted",
		"post_id": id,
	})
}

func AdminRestorePost(c fiber.Ctx) error {
	if database.DB == nil {
		return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "database is not configured"})
	}

	id, err := strconv.ParseUint(c.Params("id"), 10, 64)
	if err != nil || id == 0 {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid post id"})
	}

	var post models.Post
	if err := database.DB.Unscoped().First(&post, id).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "post not found"})
		}
		return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"})
	}

	// Check if soft-deleted
	if !post.DeletedAt.Valid {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "post is not soft-deleted"})
	}

	if err := database.DB.Unscoped().Model(&models.Post{}).Where("id = ?", id).Update("deleted_at", nil).Error; err != nil {
		return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "post could not be restored"})
	}

	return c.JSON(fiber.Map{
		"message": "post restored successfully",
		"post_id": id,
	})
}

// -------- CATEGORIES --------

// ListCategories godoc
// @Summary List categories (public)
// @Tags Categories
// @Produce json
// @Success 200 {array} models.CategoryDoc
// @Router /api/v1/categories [get]
func ListCategories(c fiber.Ctx) error {
	var cats []models.Category
	// return only root categories (no parent) and preload their immediate children
	database.DB.Preload("Children").Where("parent_id IS NULL").Find(&cats)
	return c.JSON(cats)
}

// helper: convert models.Category -> models.CategoryDoc recursively
func categoryToDoc(cat models.Category) models.CategoryDoc {
	doc := models.CategoryDoc{
		ID:          uint(cat.ID),
		Title:       cat.Title,
		Description: cat.Description,
		ParentID:    cat.ParentID,
	}
	if len(cat.Children) > 0 {
		children := make([]models.CategoryDoc, 0, len(cat.Children))
		for _, ch := range cat.Children {
			children = append(children, categoryToDoc(ch))
		}
		doc.Children = children
	}
	return doc
}

// GetCategory godoc
// @Summary Get single category (public)
// @Tags Categories
// @Produce json
// @Param slug path string true "Category slug"
// @Success 200 {object} models.CategoryDoc
// @Failure 404 {object} map[string]string
// @Router /api/v1/categories/{slug} [get]
func GetCategory(c fiber.Ctx) error {
	slug := c.Params("slug")
	if slug == "" {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid slug"})
	}
	var cat models.Category
	if err := database.DB.Preload("Children").Where("slug = ?", slug).First(&cat).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "category not found"})
		}
		return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "db error"})
	}
	doc := categoryToDoc(cat)
	return c.JSON(doc)
}

// CreateCategoryRequest represents payload for creating a category
type CreateCategoryRequest struct {
	Title       string `json:"title" validate:"required,min=2"`
	Description string `json:"description,omitempty"`
	ParentID    *uint  `json:"parent_id,omitempty"`
}


func CreateCategory(c fiber.Ctx) error {
	var input CreateCategoryRequest
	if err := c.Bind().Body(&input); err != nil {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid body"})
	}
	if err := validate.Struct(input); err != nil {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
	}
	// validate parent exists when provided
	if input.ParentID != nil {
		var parent models.Category
		if err := database.DB.First(&parent, *input.ParentID).Error; err != nil {
			return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid parent_id"})
		}
	}
	cat := models.Category{Title: input.Title, Description: input.Description, ParentID: input.ParentID}
	// generate unique slug from title
	base := slugify(input.Title)
	cat.Slug = makeUniqueSlugCategory(base)
	if err := database.DB.Create(&cat).Error; err != nil {
		return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "could not create category"})
	}
	return c.Status(http.StatusCreated).JSON(cat)
}


func UpdateCategory(c fiber.Ctx) error {
	id, err := strconv.ParseUint(c.Params("id"), 10, 64)
	if err != nil {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid id"})
	}
	var cat models.Category
	if err := database.DB.First(&cat, id).Error; err != nil {
		return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "category not found"})
	}
	var input struct {
		Title       *string `json:"title" validate:"omitempty,min=2"`
		Description *string `json:"description"`
		ParentID    *uint   `json:"parent_id"`
	}
	if err := c.Bind().Body(&input); err != nil {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid body"})
	}
	if err := validate.Struct(input); err != nil {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
	}
	if input.Title != nil {
		cat.Title = *input.Title
		// regenerate slug on title change
		cat.Slug = makeUniqueSlugCategoryExclude(slugify(*input.Title), uint64(cat.ID))
	}
	if input.Description != nil {
		cat.Description = *input.Description
	}
	if input.ParentID != nil {
		// prevent setting parent to itself
		if *input.ParentID == cat.ID {
			return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "parent_id cannot be the category itself"})
		}
		// validate parent exists
		var parent models.Category
		if err := database.DB.First(&parent, *input.ParentID).Error; err != nil {
			return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid parent_id"})
		}
		cat.ParentID = input.ParentID
	}
	if err := database.DB.Save(&cat).Error; err != nil {
		return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "could not update category"})
	}
	return c.JSON(cat)
}


func DeleteCategory(c fiber.Ctx) error {
	id, err := strconv.ParseUint(c.Params("id"), 10, 64)
	if err != nil {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid id"})
	}
	var cat models.Category
	if err := database.DB.First(&cat, id).Error; err != nil {
		return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "category not found"})
	}
	if err := database.DB.Delete(&cat).Error; err != nil {
		return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "could not delete category"})
	}
	return c.JSON(fiber.Map{"message": "category deleted"})
}

func AdminListCategories(c fiber.Ctx) error {
	trashed := c.Query("trashed", "none")
	if trashed != "none" && trashed != "only" && trashed != "with" {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid trashed parameter, must be one of: none, only, with"})
	}

	pageStr := c.Query("page", "1")
	perPageStr := c.Query("per_page", "10")
	page, _ := strconv.Atoi(pageStr)
	perPage, _ := strconv.Atoi(perPageStr)
	if page < 1 {
		page = 1
	}
	if perPage < 1 {
		perPage = 10
	}
	if perPage > 100 {
		perPage = 100
	}
	offset := (page - 1) * perPage

	var total int64
	var cats []models.Category
	db := database.DB

	switch trashed {
	case "none":
		db.Model(&models.Category{}).Count(&total)
		db.Preload("Children").Order("id desc").Limit(perPage).Offset(offset).Find(&cats)
	case "only":
		db.Unscoped().Model(&models.Category{}).Where("deleted_at IS NOT NULL").Count(&total)
		db.Unscoped().Preload("Children").Where("deleted_at IS NOT NULL").Order("deleted_at desc").Limit(perPage).Offset(offset).Find(&cats)
	case "with":
		db.Unscoped().Model(&models.Category{}).Count(&total)
		db.Unscoped().Preload("Children").Order("id desc").Limit(perPage).Offset(offset).Find(&cats)
	}

	return c.JSON(fiber.Map{"data": cats, "meta": fiber.Map{"page": page, "per_page": perPage, "total": total}})
}

func HardDeleteCategory(c fiber.Ctx) error {
	if database.DB == nil {
		return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "database is not configured"})
	}

	id, err := strconv.ParseUint(c.Params("id"), 10, 64)
	if err != nil || id == 0 {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid category id"})
	}

	var cat models.Category
	if err := database.DB.Unscoped().First(&cat, id).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "category not found"})
		}
		return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"})
	}

	err = database.DB.Transaction(func(tx *gorm.DB) error {
		// set parent_id of children to NULL to avoid FK issues
		if err := tx.Model(&models.Category{}).Where("parent_id = ?", id).Update("parent_id", nil).Error; err != nil {
			return err
		}
		// clear many2many association with posts to avoid FK constraint errors (post_categories)
		if err := tx.Model(&cat).Association("Posts").Clear(); err != nil {
			return err
		}
		return tx.Unscoped().Delete(&cat).Error
	})
	if err != nil {
		return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "category hard-delete failed"})
	}

	return c.JSON(fiber.Map{"message": "category permanently deleted", "category_id": id})
}

func AdminRestoreCategory(c fiber.Ctx) error {
	if database.DB == nil {
		return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "database is not configured"})
	}

	id, err := strconv.ParseUint(c.Params("id"), 10, 64)
	if err != nil || id == 0 {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid category id"})
	}

	var cat models.Category
	if err := database.DB.Unscoped().First(&cat, id).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "category not found"})
		}
		return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"})
	}

	if !cat.DeletedAt.Valid {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "category is not soft-deleted"})
	}

	if err := database.DB.Unscoped().Model(&models.Category{}).Where("id = ?", id).Update("deleted_at", nil).Error; err != nil {
		return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "category could not be restored"})
	}

	return c.JSON(fiber.Map{"message": "category restored successfully", "category_id": id})
}

// -------- TAGS --------

// ListTags godoc
// @Summary List tags (public)
// @Tags Tags
// @Produce json
// @Success 200 {array} models.TagDoc
// @Router /api/v1/tags [get]
func ListTags(c fiber.Ctx) error {
	var tags []models.Tag
	database.DB.Find(&tags)
	return c.JSON(tags)
}

// CreateTagRequest represents payload for creating a tag
// swagger:model CreateTagRequest
type CreateTagRequest struct {
	Name string `json:"name" validate:"required,min=1"`
}

// UpdateTagRequest represents payload for updating a tag
// swagger:model UpdateTagRequest
type UpdateTagRequest struct {
	Name *string `json:"name" validate:"omitempty,min=1"`
}


func CreateTag(c fiber.Ctx) error {
	var input CreateTagRequest
	if err := c.Bind().Body(&input); err != nil {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid body"})
	}
	if err := validate.Struct(input); err != nil {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
	}
	tag := models.Tag{Name: input.Name}
	if err := database.DB.Create(&tag).Error; err != nil {
		return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "could not create tag"})
	}
	return c.Status(http.StatusCreated).JSON(tag)
}


func UpdateTag(c fiber.Ctx) error {
	id, err := strconv.ParseUint(c.Params("id"), 10, 64)
	if err != nil {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid id"})
	}
	var tag models.Tag
	if err := database.DB.First(&tag, id).Error; err != nil {
		return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "tag not found"})
	}
	var input UpdateTagRequest
	if err := c.Bind().Body(&input); err != nil {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid body"})
	}
	if err := validate.Struct(input); err != nil {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
	}
	if input.Name != nil {
		tag.Name = *input.Name
	}
	if err := database.DB.Save(&tag).Error; err != nil {
		return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "could not update tag"})
	}
	return c.JSON(tag)
}


func DeleteTag(c fiber.Ctx) error {
	id, err := strconv.ParseUint(c.Params("id"), 10, 64)
	if err != nil {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid id"})
	}
	var tag models.Tag
	if err := database.DB.First(&tag, id).Error; err != nil {
		return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "tag not found"})
	}
	if err := database.DB.Delete(&tag).Error; err != nil {
		return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "could not delete tag"})
	}
	return c.JSON(fiber.Map{"message": "tag deleted"})
}

func AdminListTags(c fiber.Ctx) error {
	trashed := c.Query("trashed", "none")
	if trashed != "none" && trashed != "only" && trashed != "with" {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid trashed parameter, must be one of: none, only, with"})
	}

	pageStr := c.Query("page", "1")
	perPageStr := c.Query("per_page", "10")
	page, _ := strconv.Atoi(pageStr)
	perPage, _ := strconv.Atoi(perPageStr)
	if page < 1 {
		page = 1
	}
	if perPage < 1 {
		perPage = 10
	}
	if perPage > 100 {
		perPage = 100
	}
	offset := (page - 1) * perPage

	var total int64
	var tags []models.Tag
	db := database.DB

	switch trashed {
	case "none":
		db.Model(&models.Tag{}).Count(&total)
		db.Preload("Posts").Order("id desc").Limit(perPage).Offset(offset).Find(&tags)
	case "only":
		db.Unscoped().Model(&models.Tag{}).Where("deleted_at IS NOT NULL").Count(&total)
		db.Unscoped().Preload("Posts").Where("deleted_at IS NOT NULL").Order("deleted_at desc").Limit(perPage).Offset(offset).Find(&tags)
	case "with":
		db.Unscoped().Model(&models.Tag{}).Count(&total)
		db.Unscoped().Preload("Posts").Order("id desc").Limit(perPage).Offset(offset).Find(&tags)
	}

	return c.JSON(fiber.Map{"data": tags, "meta": fiber.Map{"page": page, "per_page": perPage, "total": total}})
}

func HardDeleteTag(c fiber.Ctx) error {
	if database.DB == nil {
		return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "database is not configured"})
	}

	id, err := strconv.ParseUint(c.Params("id"), 10, 64)
	if err != nil || id == 0 {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid tag id"})
	}

	var tag models.Tag
	if err := database.DB.Unscoped().First(&tag, id).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "tag not found"})
		}
		return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"})
	}

	err = database.DB.Transaction(func(tx *gorm.DB) error {
		// clear many2many association
		if err := tx.Model(&tag).Association("Posts").Clear(); err != nil {
			return err
		}
		return tx.Unscoped().Delete(&tag).Error
	})
	if err != nil {
		return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "tag hard-delete failed"})
	}

	return c.JSON(fiber.Map{"message": "tag permanently deleted", "tag_id": id})
}

func AdminRestoreTag(c fiber.Ctx) error {
	if database.DB == nil {
		return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "database is not configured"})
	}

	id, err := strconv.ParseUint(c.Params("id"), 10, 64)
	if err != nil || id == 0 {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid tag id"})
	}

	var tag models.Tag
	if err := database.DB.Unscoped().First(&tag, id).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "tag not found"})
		}
		return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"})
	}

	if !tag.DeletedAt.Valid {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "tag is not soft-deleted"})
	}

	if err := database.DB.Unscoped().Model(&models.Tag{}).Where("id = ?", id).Update("deleted_at", nil).Error; err != nil {
		return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "tag could not be restored"})
	}

	return c.JSON(fiber.Map{"message": "tag restored successfully", "tag_id": id})
}

func AdminListCategoryViews(c fiber.Ctx) error {
	trashed := c.Query("trashed", "none")
	if trashed != "none" && trashed != "only" && trashed != "with" {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid trashed parameter, must be one of: none, only, with"})
	}

	pageStr := c.Query("page", "1")
	perPageStr := c.Query("per_page", "10")
	page, _ := strconv.Atoi(pageStr)
	perPage, _ := strconv.Atoi(perPageStr)
	if page < 1 {
		page = 1
	}
	if perPage < 1 {
		perPage = 10
	}
	if perPage > 100 {
		perPage = 100
	}
	offset := (page - 1) * perPage

	var total int64
	var cvs []models.CategoryView
	db := database.DB

	switch trashed {
	case "none":
		db.Model(&models.CategoryView{}).Count(&total)
		db.Order("id desc").Limit(perPage).Offset(offset).Find(&cvs)
	case "only":
		db.Unscoped().Model(&models.CategoryView{}).Where("deleted_at IS NOT NULL").Count(&total)
		db.Unscoped().Where("deleted_at IS NOT NULL").Order("deleted_at desc").Limit(perPage).Offset(offset).Find(&cvs)
	case "with":
		db.Unscoped().Model(&models.CategoryView{}).Count(&total)
		db.Unscoped().Order("id desc").Limit(perPage).Offset(offset).Find(&cvs)
	}

	return c.JSON(fiber.Map{"data": cvs, "meta": fiber.Map{"page": page, "per_page": perPage, "total": total}})
}

func HardDeleteCategoryView(c fiber.Ctx) error {
	if database.DB == nil {
		return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "database is not configured"})
	}

	id, err := strconv.ParseUint(c.Params("id"), 10, 64)
	if err != nil || id == 0 {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid id"})
	}

	var cv models.CategoryView
	if err := database.DB.Unscoped().First(&cv, id).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "category view not found"})
		}
		return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"})
	}

	if err := database.DB.Unscoped().Delete(&cv).Error; err != nil {
		return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "category view hard-delete failed"})
	}

	return c.JSON(fiber.Map{"message": "category view permanently deleted", "id": id})
}

func AdminRestoreCategoryView(c fiber.Ctx) error {
	if database.DB == nil {
		return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "database is not configured"})
	}

	id, err := strconv.ParseUint(c.Params("id"), 10, 64)
	if err != nil || id == 0 {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid id"})
	}

	var cv models.CategoryView
	if err := database.DB.Unscoped().First(&cv, id).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "category view not found"})
		}
		return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"})
	}

	if !cv.DeletedAt.Valid {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "category view is not soft-deleted"})
	}

	if err := database.DB.Unscoped().Model(&models.CategoryView{}).Where("id = ?", id).Update("deleted_at", nil).Error; err != nil {
		return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "category view could not be restored"})
	}

	return c.JSON(fiber.Map{"message": "category view restored successfully", "id": id})
}

// -------- COMMENTS --------

// CreateComment godoc
// @Summary Create comment (public)
// @Tags Comments
// @Accept json
// @Produce json
// @Param data body object true "Comment payload"
// @Success 201 {object} models.CommentDoc
// @Failure 400 {object} map[string]string
// @Router /api/v1/comments [post]
func CreateComment(c fiber.Ctx) error {
	var input struct {
		UserID uint   `json:"user_id" validate:"required"`
		PostID uint   `json:"post_id" validate:"required"`
		Body   string `json:"body" validate:"required,min=1"`
	}
	if err := c.Bind().Body(&input); err != nil {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid body"})
	}
	if err := validate.Struct(input); err != nil {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
	}
	cm := models.Comment{UserID: input.UserID, PostID: input.PostID, Body: input.Body}
	if err := database.DB.Create(&cm).Error; err != nil {
		return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "could not create comment"})
	}
	return c.Status(http.StatusCreated).JSON(cm)
}

// ListComments godoc
// @Summary List comments for a post (public)
// @Tags Comments
// @Produce json
// @Param post_id query int true "Post ID"
// @Success 200 {array} models.CommentDoc
// @Router /api/v1/comments [get]
func ListComments(c fiber.Ctx) error {
	postIDStr := c.Query("post_id")
	if postIDStr == "" {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "post_id required"})
	}
	postID, err := strconv.ParseUint(postIDStr, 10, 64)
	if err != nil {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid post_id"})
	}
	var comments []models.Comment
	database.DB.Where("post_id = ?", postID).Find(&comments)
	return c.JSON(comments)
}


func DeleteComment(c fiber.Ctx) error {
	id, err := strconv.ParseUint(c.Params("id"), 10, 64)
	if err != nil {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid id"})
	}
	var cm models.Comment
	if err := database.DB.First(&cm, id).Error; err != nil {
		return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "comment not found"})
	}
	if err := database.DB.Delete(&cm).Error; err != nil {
		return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "could not delete comment"})
	}
	return c.JSON(fiber.Map{"message": "comment deleted"})
}
