using AutoMapper;
using PowderCoating.Shared.Constants;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using PowderCoating.Application.DTOs.Catalog;
using PowderCoating.Core.Entities;
using PowderCoating.Core.Interfaces;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace PowderCoating.Web.Controllers
{
    /// <summary>
    /// Manages the hierarchical catalog category tree used to organise catalog items.
    /// Categories form an unlimited-depth tree via the nullable <c>ParentCategoryId</c> FK;
    /// root categories have <c>ParentCategoryId == null</c>.
    /// The controller enforces:
    ///   - Case-insensitive uniqueness of names within the same parent scope.
    ///   - Circular-reference prevention when re-parenting a category.
    ///   - Non-empty precondition before soft-deleting (items and sub-categories must be cleared first).
    /// </summary>
    [Authorize(Policy = AppConstants.Policies.CanManageProducts)]
    public class CatalogCategoriesController : Controller
    {
        private readonly IUnitOfWork _unitOfWork;
        private readonly IMapper _mapper;
        private readonly ITenantContext _tenantContext;
        private readonly ILogger<CatalogCategoriesController> _logger;

        public CatalogCategoriesController(
            IUnitOfWork unitOfWork,
            IMapper mapper,
            ITenantContext tenantContext,
            ILogger<CatalogCategoriesController> logger)
        {
            _unitOfWork = unitOfWork;
            _mapper = mapper;
            _tenantContext = tenantContext;
            _logger = logger;
        }

        /// <summary>
        /// Displays the full category list, structured as a tree for hierarchical presentation.
        /// Eager-loads <c>ParentCategory</c>, <c>SubCategories</c>, and <c>Items</c> in a single
        /// round-trip so the view can render parent breadcrumbs, child counts, and item counts
        /// without issuing per-row N+1 queries.
        /// Root categories (those with no parent) are passed separately via <c>ViewBag.RootCategories</c>
        /// so the view can render them as tree roots without re-filtering in Razor.
        /// </summary>
        // GET: CatalogCategories
        public async Task<IActionResult> Index()
        {
            try
            {
                var indexCompanyId = _tenantContext.GetCurrentCompanyId() ?? 0;
                var categories = await _unitOfWork.CatalogCategories
                    .FindAsync(c => c.CompanyId == indexCompanyId, false,
                        c => c.ParentCategory,
                        c => c.SubCategories,
                        c => c.Items);

                var categoryDtos = _mapper.Map<List<CategoryDto>>(categories);

                // Build tree structure for display
                var rootCategories = categoryDtos
                    .Where(c => c.ParentCategoryId == null)
                    .OrderBy(c => c.DisplayOrder)
                    .ThenBy(c => c.Name)
                    .ToList();

                ViewBag.RootCategories = rootCategories;
                ViewBag.AllCategories = categoryDtos;

                return View(categoryDtos);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error loading catalog categories");
                TempData["Error"] = "An error occurred while loading catalog categories.";
                return View(new List<CategoryDto>());
            }
        }

        /// <summary>
        /// Shows details for a single category, including its parent breadcrumb and the list of
        /// non-deleted catalog items directly assigned to it.
        /// Items are filtered and sorted in memory (after the EF load) because the Items collection
        /// is already materialized via eager loading; a second DB query would be redundant.
        /// </summary>
        /// <param name="id">Primary key of the category to display.</param>
        // GET: CatalogCategories/Details/5
        public async Task<IActionResult> Details(int id)
        {
            try
            {
                var category = await _unitOfWork.CatalogCategories.GetByIdAsync(
                    id,
                    false,
                    c => c.ParentCategory,
                    c => c.SubCategories,
                    c => c.Items);

                if (category == null)
                {
                    TempData["Error"] = "Category not found.";
                    return RedirectToAction(nameof(Index));
                }

                var categoryDto = _mapper.Map<CategoryDto>(category);

                // Get items in this category
                var items = category.Items
                    .Where(i => !i.IsDeleted)
                    .OrderBy(i => i.DisplayOrder)
                    .ThenBy(i => i.Name)
                    .ToList();

                ViewBag.Items = items;

                return View(categoryDto);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error loading category {CategoryId}", id);
                TempData["Error"] = "An error occurred while loading the category.";
                return RedirectToAction(nameof(Index));
            }
        }

        /// <summary>
        /// Renders the category creation form, pre-populating the parent category dropdown via
        /// <see cref="PopulateParentCategoryDropdown"/> with no exclusions (no category to exclude yet).
        /// </summary>
        // GET: CatalogCategories/Create
        public async Task<IActionResult> Create()
        {
            try
            {
                await PopulateParentCategoryDropdown();
                return View(new CreateCategoryDto());
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error loading create category page");
                TempData["Error"] = "An error occurred while loading the page.";
                return RedirectToAction(nameof(Index));
            }
        }

        /// <summary>
        /// Handles category creation.  Before persisting, enforces a case-insensitive uniqueness
        /// constraint on the category name within the same parent scope (root or a specific parent).
        /// This check is done in application code rather than a DB unique index because the constraint
        /// is scoped to <c>(Name, ParentCategoryId)</c> with nullable <c>ParentCategoryId</c>, which
        /// SQL Server unique indexes do not enforce consistently for NULLs.
        /// </summary>
        /// <param name="dto">Form-bound DTO with the new category's name, description, parent, and display order.</param>
        // POST: CatalogCategories/Create
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Create(CreateCategoryDto dto)
        {
            try
            {
                if (ModelState.IsValid)
                {
                    // Check for duplicate category name under the same parent (case-insensitive)
                    var companyId = _tenantContext.GetCurrentCompanyId() ?? 0;
                    var allCategories = await _unitOfWork.CatalogCategories.FindAsync(c => c.CompanyId == companyId);
                    var existingCategory = allCategories.FirstOrDefault(c =>
                        c.Name.Equals(dto.Name.Trim(), StringComparison.OrdinalIgnoreCase) &&
                        c.ParentCategoryId == dto.ParentCategoryId);

                    if (existingCategory != null)
                    {
                        var parentInfo = dto.ParentCategoryId.HasValue
                            ? $"under the same parent category"
                            : "at the root level";
                        ModelState.AddModelError("Name", $"A category named '{existingCategory.Name}' already exists {parentInfo}.");
                        await PopulateParentCategoryDropdown();
                        return View(dto);
                    }

                    var category = _mapper.Map<CatalogCategory>(dto);
                    await _unitOfWork.CatalogCategories.AddAsync(category);
                    await _unitOfWork.CompleteAsync();

                    TempData["Success"] = $"Category '{category.Name}' created successfully.";
                    return RedirectToAction(nameof(Index));
                }

                await PopulateParentCategoryDropdown();
                return View(dto);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error creating category");
                ModelState.AddModelError("", "An error occurred while creating the category.");
                await PopulateParentCategoryDropdown();
                return View(dto);
            }
        }

        /// <summary>
        /// Renders the edit form for an existing category.
        /// Passes the category's own ID to <see cref="PopulateParentCategoryDropdown"/> so that
        /// the category itself and all its descendants are excluded from the parent dropdown,
        /// preventing the user from accidentally selecting a circular parent relationship.
        /// </summary>
        /// <param name="id">Primary key of the category to edit.</param>
        // GET: CatalogCategories/Edit/5
        public async Task<IActionResult> Edit(int id)
        {
            try
            {
                var category = await _unitOfWork.CatalogCategories.GetByIdAsync(id);
                if (category == null)
                {
                    TempData["Error"] = "Category not found.";
                    return RedirectToAction(nameof(Index));
                }

                var dto = _mapper.Map<UpdateCategoryDto>(category);
                await PopulateParentCategoryDropdown(id);

                return View(dto);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error loading category {CategoryId} for editing", id);
                TempData["Error"] = "An error occurred while loading the category.";
                return RedirectToAction(nameof(Index));
            }
        }

        /// <summary>
        /// Handles category update.  Two safety checks are performed before saving:
        /// <list type="number">
        ///   <item>Duplicate name check — only re-evaluated when the name or parent actually changed,
        ///         to avoid unnecessary DB round-trips on cosmetic edits.</item>
        ///   <item>Circular reference check — verifies that the proposed new parent is not the category
        ///         itself or any of its descendants, which would create an infinite loop in tree traversal.</item>
        /// </list>
        /// The route ID and DTO ID are cross-validated at entry to prevent tampered requests from
        /// silently editing the wrong record.
        /// </summary>
        /// <param name="id">Route ID of the category being edited.</param>
        /// <param name="dto">Form-bound DTO with updated fields.</param>
        // POST: CatalogCategories/Edit/5
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Edit(int id, UpdateCategoryDto dto)
        {
            if (id != dto.Id)
            {
                TempData["Error"] = "Invalid category ID.";
                return RedirectToAction(nameof(Index));
            }

            try
            {
                if (ModelState.IsValid)
                {
                    var category = await _unitOfWork.CatalogCategories.GetByIdAsync(id);
                    if (category == null)
                    {
                        TempData["Error"] = "Category not found.";
                        return RedirectToAction(nameof(Index));
                    }

                    // Check for duplicate category name under the same parent (only if name or parent changed)
                    var nameChanged = !category.Name.Equals(dto.Name.Trim(), StringComparison.OrdinalIgnoreCase);
                    var parentChanged = category.ParentCategoryId != dto.ParentCategoryId;

                    if (nameChanged || parentChanged)
                    {
                        var editCompanyId = _tenantContext.GetCurrentCompanyId() ?? 0;
                        var allCategories = await _unitOfWork.CatalogCategories.FindAsync(c => c.CompanyId == editCompanyId);
                        var existingCategory = allCategories.FirstOrDefault(c =>
                            c.Id != id &&
                            c.Name.Equals(dto.Name.Trim(), StringComparison.OrdinalIgnoreCase) &&
                            c.ParentCategoryId == dto.ParentCategoryId);

                        if (existingCategory != null)
                        {
                            var parentInfo = dto.ParentCategoryId.HasValue
                                ? $"under the same parent category"
                                : "at the root level";
                            ModelState.AddModelError("Name", $"A category named '{existingCategory.Name}' already exists {parentInfo}.");
                            await PopulateParentCategoryDropdown(id);
                            return View(dto);
                        }
                    }

                    // Check for circular reference
                    if (dto.ParentCategoryId.HasValue)
                    {
                        if (await WouldCreateCircularReference(id, dto.ParentCategoryId.Value))
                        {
                            ModelState.AddModelError("ParentCategoryId",
                                "Cannot set this category as parent - it would create a circular reference.");
                            await PopulateParentCategoryDropdown(id);
                            return View(dto);
                        }
                    }

                    _mapper.Map(dto, category);
                    await _unitOfWork.CompleteAsync();

                    TempData["Success"] = $"Category '{category.Name}' updated successfully.";
                    return RedirectToAction(nameof(Details), new { id = category.Id });
                }

                await PopulateParentCategoryDropdown(id);
                return View(dto);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error updating category {CategoryId}", id);
                ModelState.AddModelError("", "An error occurred while updating the category.");
                await PopulateParentCategoryDropdown(id);
                return View(dto);
            }
        }

        /// <summary>
        /// Renders the delete confirmation page for a category.
        /// Eagerly loads sub-categories and items to determine whether the category is safe to
        /// delete.  The <c>ViewBag.CanDelete</c> flag is passed to the view to conditionally
        /// render a disabled "Delete" button when the category is not empty, providing a clear
        /// UI hint rather than a post-submission error.
        /// </summary>
        /// <param name="id">Primary key of the category to delete.</param>
        // GET: CatalogCategories/Delete/5
        public async Task<IActionResult> Delete(int id)
        {
            try
            {
                var category = await _unitOfWork.CatalogCategories.GetByIdAsync(
                    id,
                    false,
                    c => c.ParentCategory,
                    c => c.SubCategories,
                    c => c.Items);

                if (category == null)
                {
                    TempData["Error"] = "Category not found.";
                    return RedirectToAction(nameof(Index));
                }

                var dto = _mapper.Map<CategoryDto>(category);

                // Check if category has items or subcategories
                var hasItems = category.Items.Any(i => !i.IsDeleted);
                var hasSubCategories = category.SubCategories.Any(c => !c.IsDeleted);

                ViewBag.HasItems = hasItems;
                ViewBag.HasSubCategories = hasSubCategories;
                ViewBag.CanDelete = !hasItems && !hasSubCategories;

                return View(dto);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error loading category {CategoryId} for deletion", id);
                TempData["Error"] = "An error occurred while loading the category.";
                return RedirectToAction(nameof(Index));
            }
        }

        /// <summary>
        /// Confirms and executes the soft-delete of a category.
        /// Re-checks emptiness on the POST to defend against TOCTOU (time-of-check/time-of-use)
        /// race conditions: an item or sub-category could have been added between the GET and POST.
        /// A soft-delete (<see cref="IRepository{T}.SoftDeleteAsync"/>) is used rather than a physical
        /// delete so the record can be recovered if needed and audit history is preserved.
        /// </summary>
        /// <param name="id">Primary key of the category to soft-delete.</param>
        // POST: CatalogCategories/Delete/5
        [HttpPost, ActionName("Delete")]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> DeleteConfirmed(int id)
        {
            try
            {
                var category = await _unitOfWork.CatalogCategories.GetByIdAsync(
                    id,
                    false,
                    c => c.SubCategories,
                    c => c.Items);

                if (category == null)
                {
                    TempData["Error"] = "Category not found.";
                    return RedirectToAction(nameof(Index));
                }

                // Verify category is empty
                var hasItems = category.Items.Any(i => !i.IsDeleted);
                var hasSubCategories = category.SubCategories.Any(c => !c.IsDeleted);

                if (hasItems || hasSubCategories)
                {
                    TempData["Error"] = "Cannot delete category that contains items or subcategories. " +
                        "Please move or delete them first.";
                    return RedirectToAction(nameof(Delete), new { id });
                }

                var categoryName = category.Name;
                await _unitOfWork.CatalogCategories.SoftDeleteAsync(id);
                await _unitOfWork.CompleteAsync();

                TempData["Success"] = $"Category '{categoryName}' deleted successfully.";
                return RedirectToAction(nameof(Index));
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error deleting category {CategoryId}", id);
                TempData["Error"] = "An error occurred while deleting the category.";
                return RedirectToAction(nameof(Index));
            }
        }

        /// <summary>
        /// AJAX endpoint that creates a category inline from the catalog item create/edit form,
        /// allowing users to add a new category without navigating away.
        /// Returns a JSON result with the new category's ID and name so the caller can append it
        /// to the category dropdown and select it immediately.
        /// Applies the same case-insensitive uniqueness guard as the full Create action.
        /// Does not validate CSRF via <c>[ValidateAntiForgeryToken]</c> because the request is
        /// a JSON POST from JavaScript (a standard AJAX anti-CSRF token approach is expected in the
        /// JS client via the <c>RequestVerificationToken</c> header).
        /// </summary>
        /// <param name="request">JSON body with name, optional description, and optional parent ID.</param>
        // AJAX: Quick create category (for use in catalog item forms)
        [HttpPost]
        public async Task<IActionResult> QuickCreate([FromBody] QuickCreateCategoryRequest request)
        {
            try
            {
                if (string.IsNullOrWhiteSpace(request.Name))
                {
                    return Json(new { success = false, message = "Category name is required." });
                }

                var trimmedName = request.Name.Trim();

                // Check for duplicate category name under the same parent (case-insensitive)
                var quickCompanyId = _tenantContext.GetCurrentCompanyId() ?? 0;
                var allCategories = await _unitOfWork.CatalogCategories.FindAsync(c => c.CompanyId == quickCompanyId);
                var existingCategory = allCategories.FirstOrDefault(c =>
                    c.Name.Equals(trimmedName, StringComparison.OrdinalIgnoreCase) &&
                    c.ParentCategoryId == request.ParentCategoryId);

                if (existingCategory != null)
                {
                    var parentInfo = request.ParentCategoryId.HasValue
                        ? "under the same parent category"
                        : "at the root level";
                    return Json(new
                    {
                        success = false,
                        message = $"A category named '{existingCategory.Name}' already exists {parentInfo}."
                    });
                }

                var category = new CatalogCategory
                {
                    Name = trimmedName,
                    Description = request.Description?.Trim(),
                    ParentCategoryId = request.ParentCategoryId,
                    DisplayOrder = 0,
                    IsActive = true
                };

                await _unitOfWork.CatalogCategories.AddAsync(category);
                await _unitOfWork.CompleteAsync();

                return Json(new
                {
                    success = true,
                    id = category.Id,
                    name = category.Name,
                    message = $"Category '{category.Name}' created successfully."
                });
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error creating category via quick-create");
                return Json(new { success = false, message = "An error occurred while creating the category." });
            }
        }

        /// <summary>
        /// Returns the full category tree as a JSON array of <c>CategoryTreeDto</c> objects,
        /// rooted at the top-level (parentless) categories.
        /// Used by JavaScript widgets that need to render an interactive tree without a full page load.
        /// Eagerly loads <c>SubCategories</c> and <c>Items</c> so the mapper can populate child counts.
        /// </summary>
        // AJAX: Get category tree
        [HttpGet]
        public async Task<IActionResult> GetCategoryTree()
        {
            try
            {
                var treeCompanyId = _tenantContext.GetCurrentCompanyId() ?? 0;
                var categories = await _unitOfWork.CatalogCategories
                    .FindAsync(c => c.CompanyId == treeCompanyId, false, c => c.SubCategories, c => c.Items);

                // Build tree from root categories
                var rootCategories = categories
                    .Where(c => c.ParentCategoryId == null)
                    .OrderBy(c => c.DisplayOrder)
                    .ThenBy(c => c.Name)
                    .ToList();

                var tree = _mapper.Map<List<CategoryTreeDto>>(rootCategories);

                return Json(tree);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error getting category tree");
                return Json(new { error = "An error occurred while loading the category tree." });
            }
        }

        /// <summary>
        /// Returns all active categories as a flat JSON array in parent-before-child (depth-first)
        /// order, with each entry including an indented display name for use in a &lt;select&gt; dropdown.
        /// This endpoint is consumed by JavaScript dropdowns on catalog item create/edit pages,
        /// allowing dynamic refresh without a full page reload.
        /// The visual indentation uses non-breaking spaces and em dashes (built by
        /// <see cref="GetCategoryDisplayName"/>) so the hierarchy is visible even in a flat dropdown.
        /// </summary>
        // AJAX: Get categories for dropdown (hierarchical list with formatted names)
        [HttpGet]
        public async Task<IActionResult> GetCategoriesForDropdown()
        {
            try
            {
                var dropdownCompanyId = _tenantContext.GetCurrentCompanyId() ?? 0;
                var categories = (await _unitOfWork.CatalogCategories.FindAsync(c => c.CompanyId == dropdownCompanyId)).ToList();

                // Build hierarchical list (parents before children)
                var hierarchicalList = new List<CatalogCategory>();
                BuildHierarchicalCategoryList(categories, null, hierarchicalList, new HashSet<int>());

                var categoryOptions = hierarchicalList
                    .Select(c => new
                    {
                        id = c.Id,
                        name = c.Name,
                        displayText = GetCategoryDisplayName(c, categories)
                    })
                    .ToList();

                return Json(new { success = true, categories = categoryOptions });
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error getting categories for dropdown");
                return Json(new { success = false, error = "An error occurred while loading categories." });
            }
        }

        /// <summary>
        /// Populates <c>ViewBag.ParentCategories</c> with a hierarchically ordered
        /// <see cref="SelectListItem"/> list suitable for a Razor dropdown.
        /// When <paramref name="excludeCategoryId"/> is provided, that category and all of its
        /// descendants are excluded from the list.  This prevents the user from accidentally
        /// choosing a descendant as the new parent, which would create a circular reference
        /// and break tree traversal logic throughout the application.
        /// A "(None - Root Category)" option is prepended so users can promote a category to root level.
        /// </summary>
        /// <param name="excludeCategoryId">
        /// Optional ID of the category being edited; its full subtree is excluded from the dropdown.
        /// </param>
        private async Task PopulateParentCategoryDropdown(int? excludeCategoryId = null)
        {
            var parentDropCompanyId = _tenantContext.GetCurrentCompanyId() ?? 0;
            var categories = (await _unitOfWork.CatalogCategories.FindAsync(c => c.CompanyId == parentDropCompanyId)).ToList();

            // Exclude the current category and its descendants to prevent circular references
            var excludedIds = new HashSet<int>();
            if (excludeCategoryId.HasValue)
            {
                excludedIds.Add(excludeCategoryId.Value);
                AddDescendantIds(excludeCategoryId.Value, excludedIds, categories);
            }

            // Build hierarchical list (parents before children)
            var hierarchicalList = new List<CatalogCategory>();
            BuildHierarchicalCategoryList(categories, null, hierarchicalList, excludedIds);

            var categoryList = hierarchicalList
                .Select(c => new SelectListItem
                {
                    Value = c.Id.ToString(),
                    Text = GetCategoryDisplayName(c, categories)
                })
                .ToList();

            // Add "(None)" option for root categories
            categoryList.Insert(0, new SelectListItem { Value = "", Text = "(None - Root Category)" });

            ViewBag.ParentCategories = categoryList;
        }

        /// <summary>
        /// Recursively builds a flat list of categories in depth-first, parent-before-child order,
        /// skipping any category whose ID is in <paramref name="excludedIds"/>.
        /// The resulting list preserves the visual hierarchy when rendered as a flat dropdown
        /// (combined with indentation from <see cref="GetCategoryDisplayName"/>).
        /// </summary>
        /// <param name="allCategories">Complete set of all non-deleted categories for the tenant.</param>
        /// <param name="parentId">The parent ID whose direct children to enumerate at this recursion level; null for root.</param>
        /// <param name="result">Accumulator list; appended to in-place.</param>
        /// <param name="excludedIds">Set of category IDs to skip (the edited category and its subtree).</param>
        private void BuildHierarchicalCategoryList(List<CatalogCategory> allCategories, int? parentId, List<CatalogCategory> result, HashSet<int> excludedIds)
        {
            var children = allCategories
                .Where(c => c.ParentCategoryId == parentId && !excludedIds.Contains(c.Id))
                .OrderBy(c => c.Name)
                .ToList();

            foreach (var category in children)
            {
                result.Add(category);
                // Recursively add children
                BuildHierarchicalCategoryList(allCategories, category.Id, result, excludedIds);
            }
        }

        /// <summary>
        /// Returns a visually indented display name for a category based on its depth in the hierarchy.
        /// Root categories get no prefix; each level of nesting prepends two non-breaking spaces
        /// plus an em dash ("&amp;nbsp;&amp;nbsp;— ") so the hierarchy is visible in a flat HTML dropdown.
        /// Non-breaking spaces are used instead of regular spaces because HTML collapses regular spaces.
        /// </summary>
        /// <param name="category">The category whose display name to build.</param>
        /// <param name="allCategories">All categories, used to walk up the ancestor chain.</param>
        private string GetCategoryDisplayName(CatalogCategory category, List<CatalogCategory> allCategories)
        {
            var depth = GetCategoryDepth(category, allCategories);
            // Use em dash and non-breaking spaces for better visibility
            var prefix = depth > 0 ? new string('\u00A0', depth * 2) + "\u2014 " : "";
            return $"{prefix}{category.Name}";
        }

        /// <summary>
        /// Computes how many levels deep a category is in the hierarchy by walking up through
        /// its ancestors until a root (no parent) is reached.
        /// Returns 0 for root categories, 1 for direct children of root, etc.
        /// The loop is guarded against orphaned records (missing parent entity) by breaking on null.
        /// </summary>
        /// <param name="category">The category whose depth to compute.</param>
        /// <param name="allCategories">All categories, used to look up parent entities by ID.</param>
        private int GetCategoryDepth(CatalogCategory category, List<CatalogCategory> allCategories)
        {
            int depth = 0;
            var current = category;

            while (current.ParentCategoryId.HasValue)
            {
                depth++;
                current = allCategories.FirstOrDefault(c => c.Id == current.ParentCategoryId.Value);
                if (current == null) break;
            }

            return depth;
        }

        /// <summary>
        /// Recursively collects the IDs of all descendants of <paramref name="categoryId"/>
        /// into <paramref name="excludedIds"/>.
        /// Used by <see cref="PopulateParentCategoryDropdown"/> to build the exclusion set that
        /// prevents circular-reference selections in the parent category dropdown.
        /// </summary>
        /// <param name="categoryId">Root of the subtree to collect.</param>
        /// <param name="excludedIds">Set to add descendant IDs into; modified in-place.</param>
        /// <param name="allCategories">All categories, used to find direct children at each level.</param>
        private void AddDescendantIds(int categoryId, HashSet<int> excludedIds, List<CatalogCategory> allCategories)
        {
            var children = allCategories.Where(c => c.ParentCategoryId == categoryId).ToList();
            foreach (var child in children)
            {
                excludedIds.Add(child.Id);
                AddDescendantIds(child.Id, excludedIds, allCategories);
            }
        }

        /// <summary>
        /// Determines whether assigning <paramref name="newParentId"/> as the parent of
        /// <paramref name="categoryId"/> would create a circular reference in the category tree.
        /// A circular reference occurs when the proposed parent is the category itself, or when
        /// walking up the proposed parent's ancestor chain eventually reaches the category being moved.
        /// This check is necessary because the tree is stored as a flat table with a self-referential FK,
        /// and the database does not enforce acyclicity at the schema level.
        /// </summary>
        /// <param name="categoryId">ID of the category being re-parented.</param>
        /// <param name="newParentId">Proposed new parent ID.</param>
        /// <returns><c>true</c> if the assignment would create a cycle; <c>false</c> if it is safe.</returns>
        private async Task<bool> WouldCreateCircularReference(int categoryId, int newParentId)
        {
            if (categoryId == newParentId)
                return true;

            var circleCompanyId = _tenantContext.GetCurrentCompanyId() ?? 0;
            var categories = (await _unitOfWork.CatalogCategories.FindAsync(c => c.CompanyId == circleCompanyId)).ToList();
            var current = categories.FirstOrDefault(c => c.Id == newParentId);

            while (current != null)
            {
                if (current.Id == categoryId)
                    return true;

                if (!current.ParentCategoryId.HasValue)
                    break;

                current = categories.FirstOrDefault(c => c.Id == current.ParentCategoryId.Value);
            }

            return false;
        }
    }

    /// <summary>
    /// Request body model for the <see cref="CatalogCategoriesController.QuickCreate"/> AJAX endpoint.
    /// Bound from the JSON body of the POST request.
    /// </summary>
    public class QuickCreateCategoryRequest
    {
        /// <summary>The display name for the new category. Must not be null or whitespace.</summary>
        public string Name { get; set; } = string.Empty;

        /// <summary>Optional description for the new category.</summary>
        public string? Description { get; set; }

        /// <summary>
        /// Optional parent category ID.  When null the new category is created at the root level.
        /// </summary>
        public int? ParentCategoryId { get; set; }
    }
}
