/**
 * Categories Service
 * Business logic for category management
 */

// Import Category and related models
const { Category, ProductCategory, Product } = require('../../../models');
// Import custom error classes
const { NotFoundError, ValidationError, ConflictError } = require('../../../utils/errors');
// Import logger for logging
const logger = require('../../../utils/logger');
// Import Sequelize operators
const { Op } = require('sequelize');
// Import slug generation utility
const slugify = require('slugify');

/**
 * Generate slug from name
 * Generates a URL-friendly slug from category name
 * @param {string} name - Category name
 * @returns {string} Generated slug
 */
const generateSlug = (name) => {
  // Generate slug from name
  const slug = slugify(name, {
    lower: true, // Convert to lowercase
    strict: true, // Remove special characters
    trim: true, // Trim whitespace
  });
  return slug; // Return slug
};

/**
 * Create category
 * Creates a new category
 * @param {Object} categoryData - Category data (name, parent_id, description, image_url, sort_order)
 * @returns {Promise<Object>} Created category
 */
const createCategory = async (categoryData) => {
  // Extract category data
  const { name, parent_id = null, description = null, image_url = null, sort_order = 0, slug = null } = categoryData; // Extract data
  
  // Validate required fields
  if (!name) {
    throw new ValidationError('Category name is required'); // Throw error if name missing
  }
  
  // Generate slug if not provided
  let categorySlug = slug; // Use provided slug
  if (!categorySlug) {
    categorySlug = generateSlug(name); // Generate slug from name
  }
  
  // Check if slug already exists
  const existingCategory = await Category.findOne({
    where: { slug: categorySlug }, // Match slug
  });
  
  if (existingCategory) {
    // If slug exists, append number to make it unique
    let uniqueSlug = categorySlug; // Start with original slug
    let counter = 1; // Counter for uniqueness
    while (await Category.findOne({ where: { slug: uniqueSlug } })) {
      uniqueSlug = `${categorySlug}-${counter}`; // Append counter
      counter++; // Increment counter
    }
    categorySlug = uniqueSlug; // Use unique slug
  }
  
  // Validate parent category if provided
  if (parent_id) {
    const parentCategory = await Category.findByPk(parent_id); // Find parent category
    if (!parentCategory) {
      throw new NotFoundError('Parent category not found'); // Throw error if parent doesn't exist
    }
    
    // Prevent circular references (category cannot be its own parent)
    if (parent_id === categoryData.id) {
      throw new ValidationError('Category cannot be its own parent'); // Throw error if circular reference
    }
  }
  
  // Create category
  const category = await Category.create({
    name, // Set name
    slug: categorySlug, // Set slug
    parent_id, // Set parent ID (can be null)
    description, // Set description
    image_url, // Set image URL
    sort_order, // Set sort order
  });
  
  // Reload category with associations
  const categoryWithDetails = await Category.findByPk(category.id, {
    include: [
      {
        model: Category, // Include parent category
        as: 'parent', // Use parent alias
        required: false, // Optional join
      },
    ],
  });
  
  // Log category creation
  logger.info(`Category created: ${name}`, {
    categoryId: category.id,
    name,
    parentId: parent_id,
    slug: categorySlug,
  });
  
  // Return created category
  return categoryWithDetails; // Return category
};

/**
 * Get category by ID
 * Retrieves a category with details
 * @param {number} categoryId - Category ID
 * @returns {Promise<Object>} Category with associations
 */
const getCategory = async (categoryId) => {
  // Find category by ID
  const category = await Category.findByPk(categoryId, {
    include: [
      {
        model: Category, // Include parent category
        as: 'parent', // Use parent alias
        required: false, // Optional join
      },
      {
        model: Category, // Include child categories
        as: 'children', // Use children alias
        required: false, // Optional join
      },
    ],
  });
  
  // If category not found, throw error
  if (!category) {
    throw new NotFoundError('Category not found'); // Throw error
  }
  
  // Return category
  return category; // Return category
};

/**
 * Update category
 * Updates an existing category
 * @param {number} categoryId - Category ID
 * @param {Object} updateData - Update data
 * @returns {Promise<Object>} Updated category
 */
const updateCategory = async (categoryId, updateData) => {
  // Find category
  const category = await Category.findByPk(categoryId); // Find category by ID
  if (!category) {
    throw new NotFoundError('Category not found'); // Throw error if category doesn't exist
  }
  
  // Extract update data
  const { name, parent_id, description, image_url, sort_order, slug } = updateData; // Extract data
  
  // Generate slug if name is being updated and slug not provided
  if (name && !slug) {
    const newSlug = generateSlug(name); // Generate slug from name
    // Check if new slug is different from current slug and already exists
    if (newSlug !== category.slug) {
      const existingCategory = await Category.findOne({
        where: {
          slug: newSlug, // Match slug
          id: { [Op.ne]: categoryId }, // Exclude current category
        },
      });
      
      if (existingCategory) {
        // If slug exists, append number to make it unique
        let uniqueSlug = newSlug; // Start with original slug
        let counter = 1; // Counter for uniqueness
        while (await Category.findOne({ where: { slug: uniqueSlug, id: { [Op.ne]: categoryId } } })) {
          uniqueSlug = `${newSlug}-${counter}`; // Append counter
          counter++; // Increment counter
        }
        updateData.slug = uniqueSlug; // Set unique slug
      } else {
        updateData.slug = newSlug; // Set new slug
      }
    }
  }
  
  // Validate parent category if provided
  if (parent_id !== undefined) {
    if (parent_id === categoryId) {
      throw new ValidationError('Category cannot be its own parent'); // Throw error if circular reference
    }
    
    if (parent_id !== null) {
      const parentCategory = await Category.findByPk(parent_id); // Find parent category
      if (!parentCategory) {
        throw new NotFoundError('Parent category not found'); // Throw error if parent doesn't exist
      }
      
      // Check for circular references (parent cannot be a descendant of this category)
      const isDescendant = await checkIfDescendant(categoryId, parent_id); // Check if parent is descendant
      if (isDescendant) {
        throw new ValidationError('Cannot set parent: would create circular reference'); // Throw error if circular reference
      }
    }
  }
  
  // Update category
  await category.update(updateData); // Update category
  
  // Reload category with associations
  const updatedCategory = await Category.findByPk(categoryId, {
    include: [
      {
        model: Category, // Include parent category
        as: 'parent', // Use parent alias
        required: false, // Optional join
      },
      {
        model: Category, // Include child categories
        as: 'children', // Use children alias
        required: false, // Optional join
      },
    ],
  });
  
  // Log category update
  logger.info(`Category updated: ${categoryId}`, {
    categoryId,
    updateData,
  });
  
  // Return updated category
  return updatedCategory; // Return category
};

/**
 * Check if a category is a descendant of another category
 * Helper function to prevent circular references
 * @param {number} ancestorId - Ancestor category ID
 * @param {number} descendantId - Potential descendant category ID
 * @returns {Promise<boolean>} True if descendant, false otherwise
 */
const checkIfDescendant = async (ancestorId, descendantId) => {
  // Get category
  const category = await Category.findByPk(descendantId); // Find category by ID
  if (!category || !category.parent_id) {
    return false; // Not a descendant if category doesn't exist or has no parent
  }
  
  // If parent is the ancestor, return true
  if (category.parent_id === ancestorId) {
    return true; // Found ancestor
  }
  
  // Recursively check parent
  return await checkIfDescendant(ancestorId, category.parent_id); // Check parent
};

/**
 * Delete category
 * Deletes a category (only if it has no children and no products)
 * @param {number} categoryId - Category ID
 * @returns {Promise<void>}
 */
const deleteCategory = async (categoryId) => {
  // Find category
  const category = await Category.findByPk(categoryId); // Find category by ID
  if (!category) {
    throw new NotFoundError('Category not found'); // Throw error if category doesn't exist
  }
  
  // Check if category has children
  const childrenCount = await Category.count({
    where: { parent_id: categoryId }, // Count children
  });
  
  if (childrenCount > 0) {
    throw new ValidationError('Cannot delete category with child categories'); // Throw error if has children
  }
  
  // Check if category has products
  const productsCount = await ProductCategory.count({
    where: { category_id: categoryId }, // Count products
  });
  
  if (productsCount > 0) {
    throw new ValidationError('Cannot delete category with assigned products'); // Throw error if has products
  }
  
  // Delete category
  await category.destroy(); // Delete category
  
  // Log category deletion
  logger.info(`Category deleted: ${categoryId}`, {
    categoryId,
    name: category.name,
  });
};

/**
 * List categories with filters
 * Retrieves paginated list of categories
 * @param {Object} options - Query options
 * @param {number} options.page - Page number (default: 1)
 * @param {number} options.limit - Items per page (default: 10)
 * @param {number|null} options.parentId - Filter by parent ID (null for root categories)
 * @param {boolean|null} options.withChildren - Include child categories
 * @returns {Promise<Object>} Paginated categories list
 */
const listCategories = async (options = {}) => {
  // Extract options with defaults
  const {
    page = 1,
    limit = 10,
    parentId = null,
    withChildren = false,
  } = options; // Extract and set defaults
  
  // Build where clause
  const where = {}; // Initialize where clause
  
  // Add parent ID filter if provided
  if (parentId !== null) {
    where.parent_id = parentId; // Filter by parent ID
  } else if (parentId === null && options.hasOwnProperty('parentId')) {
    // Explicitly filter for root categories (parent_id is null)
    where.parent_id = null; // Filter for root categories
  }
  
  // Calculate offset for pagination
  const offset = (page - 1) * limit; // Calculate offset
  
  // Build query options
  const queryOptions = {
    where, // Apply where clause
    include: [
      {
        model: Category, // Include parent category
        as: 'parent', // Use parent alias
        required: false, // Optional join
      },
    ],
    limit: parseInt(limit), // Set limit
    offset: parseInt(offset), // Set offset
    order: [['sort_order', 'ASC'], ['name', 'ASC']], // Order by sort_order then name
  };
  
  // Include children if requested
  if (withChildren) {
    queryOptions.include.push({
      model: Category, // Include child categories
      as: 'children', // Use children alias
      required: false, // Optional join
    });
  }
  
  // Execute query to get categories and total count
  const { count, rows } = await Category.findAndCountAll(queryOptions); // Get categories with count
  
  // Calculate total pages
  const totalPages = Math.ceil(count / limit); // Calculate total pages
  
  // Return paginated results
  return {
    categories: rows, // Category records
    pagination: {
      page: parseInt(page), // Current page
      limit: parseInt(limit), // Items per page
      total: count, // Total items
      totalPages, // Total pages
    },
  };
};

/**
 * Get category hierarchy
 * Retrieves full category hierarchy (tree structure)
 * @param {number|null} rootCategoryId - Root category ID (null for all root categories)
 * @returns {Promise<Array>} Category hierarchy tree
 */
const getCategoryHierarchy = async (rootCategoryId = null) => {
  // Build where clause
  const where = {}; // Initialize where clause
  
  if (rootCategoryId) {
    where.id = rootCategoryId; // Filter by root category ID
  } else {
    where.parent_id = null; // Get root categories only
  }
  
  // Get root categories
  const rootCategories = await Category.findAll({
    where, // Apply where clause
    include: [
      {
        model: Category, // Include child categories
        as: 'children', // Use children alias
        required: false, // Optional join
      },
    ],
    order: [['sort_order', 'ASC'], ['name', 'ASC']], // Order by sort_order then name
  });
  
  // Recursively build hierarchy tree
  const buildTree = async (categories) => {
    const tree = []; // Initialize tree array
    
    for (const category of categories) {
      // Get child categories
      const children = await Category.findAll({
        where: { parent_id: category.id }, // Match parent ID
        order: [['sort_order', 'ASC'], ['name', 'ASC']], // Order by sort_order then name
      });
      
      // Recursively build children tree
      const childrenTree = await buildTree(children); // Build children tree
      
      // Add category to tree with children
      tree.push({
        ...category.toJSON(), // Convert to JSON
        children: childrenTree, // Add children tree
      });
    }
    
    return tree; // Return tree
  };
  
  // Build and return hierarchy tree
  return await buildTree(rootCategories); // Return hierarchy tree
};

/**
 * Assign product to category
 * Assigns a product to one or more categories
 * @param {number} productId - Product ID
 * @param {Array<number>} categoryIds - Array of category IDs
 * @param {number|null} primaryCategoryId - Primary category ID (optional)
 * @returns {Promise<Array>} Created product-category assignments
 */
const assignProductToCategories = async (productId, categoryIds, primaryCategoryId = null) => {
  // Validate product exists
  const product = await Product.findByPk(productId); // Find product by ID
  if (!product) {
    throw new NotFoundError('Product not found'); // Throw error if product doesn't exist
  }
  
  // Validate categories exist
  for (const categoryId of categoryIds) {
    const category = await Category.findByPk(categoryId); // Find category by ID
    if (!category) {
      throw new NotFoundError(`Category not found: ${categoryId}`); // Throw error if category doesn't exist
    }
  }
  
  // Remove existing assignments (optional - comment out if you want to keep existing)
  // await ProductCategory.destroy({
  //   where: { product_id: productId }, // Match product ID
  // });
  
  // Create new assignments
  const assignments = []; // Initialize assignments array
  
  for (const categoryId of categoryIds) {
    // Check if assignment already exists
    const existingAssignment = await ProductCategory.findOne({
      where: {
        product_id: productId, // Match product ID
        category_id: categoryId, // Match category ID
      },
    });
    
    if (!existingAssignment) {
      // Create new assignment
      const assignment = await ProductCategory.create({
        product_id: productId, // Set product ID
        category_id: categoryId, // Set category ID
        is_primary: primaryCategoryId === categoryId, // Set primary flag
      });
      
      assignments.push(assignment); // Add to assignments array
    } else {
      // Update existing assignment if it should be primary
      if (primaryCategoryId === categoryId && !existingAssignment.is_primary) {
        // Remove primary flag from other assignments
        await ProductCategory.update(
          { is_primary: false }, // Set is_primary to false
          {
            where: {
              product_id: productId, // Match product ID
              is_primary: true, // Match primary flag
            },
          }
        );
        
        // Set this assignment as primary
        existingAssignment.is_primary = true; // Set primary flag
        await existingAssignment.save(); // Save assignment
      }
      
      assignments.push(existingAssignment); // Add to assignments array
    }
  }
  
  // Set primary category if specified
  if (primaryCategoryId) {
    // Remove primary flag from all assignments
    await ProductCategory.update(
      { is_primary: false }, // Set is_primary to false
      {
        where: { product_id: productId }, // Match product ID
      }
    );
    
    // Set specified category as primary
    await ProductCategory.update(
      { is_primary: true }, // Set is_primary to true
      {
        where: {
          product_id: productId, // Match product ID
          category_id: primaryCategoryId, // Match category ID
        },
      }
    );
  }
  
  // Return assignments
  return assignments; // Return assignments
};

/**
 * Remove product from category
 * Removes a product from a category
 * @param {number} productId - Product ID
 * @param {number} categoryId - Category ID
 * @returns {Promise<void>}
 */
const removeProductFromCategory = async (productId, categoryId) => {
  // Find assignment
  const assignment = await ProductCategory.findOne({
    where: {
      product_id: productId, // Match product ID
      category_id: categoryId, // Match category ID
    },
  });
  
  // If assignment not found, throw error
  if (!assignment) {
    throw new NotFoundError('Product-category assignment not found'); // Throw error
  }
  
  // Delete assignment
  await assignment.destroy(); // Delete assignment
  
  // Log removal
  logger.info(`Product removed from category: productId=${productId}, categoryId=${categoryId}`);
};

/**
 * Get product categories
 * Retrieves all categories for a product
 * @param {number} productId - Product ID
 * @returns {Promise<Array>} Product categories
 */
const getProductCategories = async (productId) => {
  // Get product categories
  const productCategories = await ProductCategory.findAll({
    where: { product_id: productId }, // Match product ID
    include: [
      {
        model: Category, // Include category details
        as: 'category', // Use category alias
      },
    ],
    order: [['is_primary', 'DESC'], ['category_id', 'ASC']], // Order by primary flag then category ID
  });
  
  // Return product categories
  return productCategories; // Return product categories
};

// Export service functions
module.exports = {
  createCategory, // Create category function
  getCategory, // Get category function
  updateCategory, // Update category function
  deleteCategory, // Delete category function
  listCategories, // List categories function
  getCategoryHierarchy, // Get category hierarchy function
  assignProductToCategories, // Assign product to categories function
  removeProductFromCategory, // Remove product from category function
  getProductCategories, // Get product categories function
};

