/**
 * GRN Service
 * Business logic for Goods Receipt Note (GRN) management
 */

// Import GRN and related models
const { GRN, GRNItem, PurchaseOrder, PurchaseOrderItem, Vendor, Product, Inventory, InventoryItem } = require('../../../models');
// Import inventory service
const inventoryService = require('../../inventory/services');
const inventoryItemsService = require('../../inventory/services/items');
// Import UID generator utility
const { generateUID, generateBarcode } = require('../../../utils/uidGenerator');
// Import InventoryMovement for audit trail
const { InventoryMovement } = require('../../../models');
// Import custom error classes
const { NotFoundError, ValidationError } = require('../../../utils/errors');
// Import logger for logging
const logger = require('../../../utils/logger');
// Import Sequelize operators and transaction
const { Op } = require('sequelize');
const { sequelize } = require('../../../models');
// Import dimension-based GRN processing service
const grnProcessingService = require('../../../services/grnProcessingService');

/**
 * Create GRN
 * Creates a new GRN (with or without PO - open-market friendly)
 * @param {Object} grnData - GRN data (purchase_order_id, vendor_id, received_at, items)
 * @param {number} userId - User ID creating the GRN
 * @returns {Promise<Object>} Created GRN with items
 */
const createGRN = async (grnData, userId) => {
  // Extract GRN data
  const {
    purchase_order_id = null, // Purchase order ID (optional)
    vendor_id = null, // Vendor ID (optional for open-market)
    received_at = new Date(), // Received date
    items = [], // GRN items
  } = grnData; // Extract data
  
  // Validate items
  if (!items || items.length === 0) {
    throw new ValidationError('GRN must have at least one item'); // Throw error if no items
  }
  
  // Validate purchase order if provided
  if (purchase_order_id) {
    const purchaseOrder = await PurchaseOrder.findByPk(purchase_order_id); // Find PO
    if (!purchaseOrder) {
      throw new NotFoundError(`Purchase order with ID ${purchase_order_id} not found`); // Throw error if PO not found
    }
    
    // If PO provided, use its vendor_id if vendor_id not explicitly provided
    if (!vendor_id && purchaseOrder.vendor_id) {
      vendor_id = purchaseOrder.vendor_id; // Use PO vendor_id
    }
  }
  
  // Validate vendor if provided
  if (vendor_id) {
    const vendor = await Vendor.findByPk(vendor_id); // Find vendor
    if (!vendor) {
      throw new NotFoundError(`Vendor with ID ${vendor_id} not found`); // Throw error if vendor not found
    }
  }
  
  // Start database transaction
  const transaction = await sequelize.transaction(); // Start transaction
  
  try {
    // Create GRN
    const grn = await GRN.create({
      purchase_order_id, // Purchase order ID (optional)
      vendor_id, // Vendor ID (optional)
      received_at: new Date(received_at), // Received date
    }, { transaction }); // Create GRN in transaction
    
    // If GRN is linked to a PO, validate quantities don't exceed remaining
    let poItemsMap = null;
    if (purchase_order_id) {
      // Get PO with items to check remaining quantities
      const po = await PurchaseOrder.findByPk(purchase_order_id, {
        include: [
          {
            model: PurchaseOrderItem,
            as: 'items',
            required: false,
          },
          {
            model: GRN,
            as: 'grns',
            required: false,
            include: [
              {
                model: GRNItem,
                as: 'items',
                required: false,
              },
            ],
          },
        ],
        transaction,
      });

      if (po) {
        // **IMPORTANT**: PO items don't have dimensions - they're matched by product_id only
        // GRN items may have dimensions (for dimension-based RM), but we sum all received quantities
        // by product_id when validating against PO
        
        // Calculate received quantities for each PO item from existing GRNs
        // Sum by product_id only (PO items don't have dimensions)
        const receivedMap = new Map();
        for (const grn of po.grns || []) {
          // Include all existing GRNs (for create, grnId is undefined; for update, exclude current)
          // This will be the GRN we're creating, so include all existing ones
          for (const grnItem of grn.items || []) {
            // Match by product_id only (PO items don't have dimensions)
            const key = `${grnItem.product_id}`;
            const currentReceived = receivedMap.get(key) || 0;
            // For dimension-based RM, sum pieces_count; for others, sum quantity
            const receivedQty = grnItem.pieces_count || parseFloat(grnItem.quantity || 0);
            receivedMap.set(key, currentReceived + receivedQty);
          }
        }

        // Build map of remaining quantities by product_id only
        poItemsMap = new Map();
        for (const poItem of po.items || []) {
          // PO items are matched by product_id only (no dimensions)
          const key = `${poItem.product_id}`;
          const orderedQty = parseFloat(poItem.quantity || 0);
          const receivedQty = receivedMap.get(key) || 0;
          const remainingQty = Math.max(0, orderedQty - receivedQty);
          poItemsMap.set(key, {
            ordered: orderedQty,
            received: receivedQty,
            remaining: remainingQty,
            poItem, // Store reference to PO item for validation
          });
        }
      }
    }
    
    // Create GRN items
    const grnItems = []; // Array to store created GRN items
    for (const item of items) {
      // Extract item data
      const { product_id, quantity, unit_cost } = item; // Extract item data (variants removed)
      
      // Validate required fields
      if (!product_id || !quantity || !unit_cost) {
        throw new ValidationError('Item must have product_id, quantity, and unit_cost'); // Throw error if missing required fields
      }
      
      const receivedQty = parseFloat(quantity);
      
      // Validate product exists first (needed for PO matching)
      const product = await Product.findByPk(product_id, { transaction }); // Find product in transaction
      if (!product) {
        throw new NotFoundError(`Product not found: ${product_id}`); // Throw error if product doesn't exist
      }
      
      // If GRN is linked to PO, validate quantity doesn't exceed remaining
      // Match by product_id only (PO items don't have dimensions)
      if (purchase_order_id && poItemsMap) {
        // PO items are matched by product_id only
        const key = `${product_id}`;
        const poItemData = poItemsMap.get(key);
        
        if (poItemData) {
          // For dimension-based RM, use pieces_count; for others, use quantity
          const receivedQtyForValidation = product.product_type === 'RM' && product.track_by_dimensions
            ? (item.pieces_count || receivedQty)
            : receivedQty;
          
          if (receivedQtyForValidation > poItemData.remaining) {
            throw new ValidationError(
              `Cannot receive ${receivedQtyForValidation} units. Only ${poItemData.remaining} remaining ` +
              `(Ordered: ${poItemData.ordered}, Already received: ${poItemData.received})`
            );
          }
        } else {
          // Item not in PO - allow it (open market GRN can have additional items)
        }
      }
      
      // Validate dimension requirements for RM products
      if (product.product_type === 'RM' && product.track_by_dimensions) {
        // RM products require dimension fields
        const { piece_length, piece_width, dimension_unit, pieces_count = 1 } = item;
        
        if (!piece_length || !piece_width || !dimension_unit) {
          throw new ValidationError(
            `RM product ${product.name} requires dimensions (piece_length, piece_width, dimension_unit)`
          );
        }
        
        // Validate dimension values using dimension validation service
        const dimensionValidation = grnProcessingService.validateGRNItemDimensions(
          { piece_length, piece_width, dimension_unit, pieces_count },
          product
        );
        
        if (!dimensionValidation.isValid) {
          throw new ValidationError(
            `Invalid dimensions for ${product.name}: ${dimensionValidation.error}`
          );
        }
        
        // Validate that quantity matches pieces_count for RM products
        if (parseFloat(quantity) !== pieces_count) {
          throw new ValidationError(
            `For RM products, quantity (${quantity}) must equal pieces_count (${pieces_count})`
          );
        }
      }
      
      // Variant validation removed - variants not used
      
      // Validate quantity and unit_cost
      if (receivedQty <= 0) {
        throw new ValidationError('Quantity must be greater than 0'); // Throw error if invalid quantity
      }
      
      if (parseFloat(unit_cost) < 0) {
        throw new ValidationError('Unit cost cannot be negative'); // Throw error if negative cost
      }
      
      // Create GRN item
      const grnItemData = {
        grn_id: grn.id, // GRN ID
        product_id, // Product ID
         // Variant ID
        quantity: parseFloat(quantity), // Quantity
        unit_cost: parseFloat(unit_cost), // Unit cost
      };
      
      // Add dimension fields for RM products
      if (product.product_type === 'RM' && product.track_by_dimensions) {
        const { piece_length, piece_width, dimension_unit, pieces_count = 1 } = item;
        grnItemData.piece_length = parseFloat(piece_length);
        grnItemData.piece_width = parseFloat(piece_width);
        grnItemData.dimension_unit = dimension_unit;
        grnItemData.pieces_count = parseInt(pieces_count);
      }
      
      const grnItem = await GRNItem.create(grnItemData, { transaction }); // Create GRN item in transaction
      
      grnItems.push(grnItem); // Add to array
    }
    
    // Commit transaction
    await transaction.commit(); // Commit transaction
    
    logger.info(`GRN created: ${grn.id} with ${grnItems.length} items`); // Log GRN creation
    
    // Return GRN with items and associations
    return await getGRN(grn.id); // Return GRN with associations
  } catch (error) {
    // Rollback transaction on error
    await transaction.rollback(); // Rollback transaction
    throw error; // Re-throw error
  }
};

/**
 * Get GRN by ID
 * Retrieves a GRN by ID with items
 * @param {number} grnId - GRN ID
 * @returns {Promise<Object>} GRN with items
 */
const getGRN = async (grnId) => {
  // Validate GRN ID
  if (!grnId) {
    throw new ValidationError('GRN ID is required'); // Throw error if ID missing
  }
  
  // Find GRN with associations
  const grn = await GRN.findByPk(grnId, {
    include: [
      {
        model: PurchaseOrder, // Include purchase order
        as: 'purchaseOrder', // Use purchaseOrder alias
        required: false, // Left join (PO may not exist)
      },
      {
        model: Vendor, // Include vendor
        as: 'vendor', // Use vendor alias
        required: false, // Left join (vendor may not exist)
      },
      {
        model: GRNItem, // Include GRN items
        as: 'items', // Use items alias
        include: [
          {
            model: Product, // Include product
            as: 'product', // Use product alias
          },
          // ProductVariant removed - variants not used
        ],
      },
    ],
  });
  
  // Check if GRN exists
  if (!grn) {
    throw new NotFoundError(`GRN with ID ${grnId} not found`); // Throw error if not found
  }
  
  // Return GRN
  return grn;
};

/**
 * List GRNs
 * Lists GRNs with optional filters
 * @param {Object} filters - Filter options (purchase_order_id, vendor_id, po_number, status, start_date, end_date)
 * @param {Object} pagination - Pagination options (page, limit)
 * @returns {Promise<Object>} Paginated list of GRNs
 */
const listGRNs = async (filters = {}, pagination = {}) => {
  // Import Sequelize operators
  const { Op } = require('sequelize');
  
  // Extract filters
  const {
    purchase_order_id, // Purchase order ID filter
    vendor_id, // Vendor ID filter
    po_number, // PO number search filter
    status, // Status filter (processed, pending)
    start_date, // Start date for date range filter
    end_date, // End date for date range filter
  } = filters; // Extract filters
  
  // Extract pagination options
  const page = parseInt(pagination.page, 10) || 1; // Current page (default 1)
  const limit = parseInt(pagination.limit, 10) || 50; // Items per page (default 50)
  const offset = (page - 1) * limit; // Calculate offset
  
  // Build where clause
  const where = {}; // Initialize where clause
  
  // Add purchase_order_id filter
  if (purchase_order_id !== undefined) {
    if (purchase_order_id === null) {
      where.purchase_order_id = null; // Filter for GRNs without PO
    } else {
      where.purchase_order_id = purchase_order_id; // Filter by PO ID
    }
  }
  
  // Add vendor_id filter
  if (vendor_id !== undefined) {
    if (vendor_id === null) {
      where.vendor_id = null; // Filter for GRNs without vendor
    } else {
      where.vendor_id = vendor_id; // Filter by vendor ID
    }
  }
  
  // Add status filter (processed vs pending based on processed_at)
  if (status) {
    if (status === 'processed') {
      where.processed_at = { [Op.ne]: null }; // Has processed_at (not null)
    } else if (status === 'pending') {
      where.processed_at = null; // No processed_at (null)
    }
  }
  
  // Add date range filter (on created_at)
  if (start_date || end_date) {
    where.created_at = {}; // Initialize date filter
    if (start_date) {
      const startDate = new Date(start_date);
      startDate.setHours(0, 0, 0, 0); // Start of day
      where.created_at[Op.gte] = startDate; // Greater than or equal to start date
    }
    if (end_date) {
      const endDate = new Date(end_date);
      endDate.setHours(23, 59, 59, 999); // End of day
      where.created_at[Op.lte] = endDate; // Less than or equal to end date
    }
  }
  
  // Build include clause with PO number filter
  const includeOptions = [
    {
      model: PurchaseOrder, // Include purchase order
      as: 'purchaseOrder', // Use purchaseOrder alias
      required: false, // Left join
      where: po_number ? {
        po_number: { [Op.like]: `%${po_number}%` } // Search PO number (case-insensitive partial match)
      } : undefined,
    },
    {
      model: Vendor, // Include vendor
      as: 'vendor', // Use vendor alias
      required: false, // Left join
    },
  ];
  
  // Remove undefined where clauses from includes
  includeOptions.forEach(include => {
    if (include.where && Object.keys(include.where).length === 0) {
      delete include.where;
    }
  });
  
  // Find GRNs with pagination
  const { count, rows } = await GRN.findAndCountAll({
    where, // Where clause
    include: includeOptions,
    limit, // Limit results
    offset, // Offset results
    order: [['created_at', 'DESC']], // Order by created date descending
  });
  
  // Calculate pagination metadata
  const totalPages = Math.ceil(count / limit); // Total pages
  const hasNextPage = page < totalPages; // Has next page
  const hasPrevPage = page > 1; // Has previous page
  
  // Return paginated results
  return {
    grns: rows, // GRNs array
    pagination: {
      page, // Current page
      limit, // Items per page
      total: count, // Total count
      totalPages, // Total pages
      hasNextPage, // Has next page
      hasPrevPage, // Has previous page
    },
  };
};

/**
 * Process GRN
 * Processes a GRN by adding stock to inventory and generating UIDs
 * Uses dimension-based processing for RM products
 * @param {number} grnId - GRN ID
 * @param {number} userId - User ID processing the GRN
 * @param {boolean} generateUIDs - Whether to generate UIDs for inventory items (default: true)
 * @returns {Promise<Object>} Processed GRN
 */
const processGRN = async (grnId, userId, generateUIDs = true) => {
  // Validate GRN ID
  if (!grnId) {
    throw new ValidationError('GRN ID is required'); // Throw error if ID missing
  }
  
  // Get GRN with items
  const grn = await GRN.findByPk(grnId, {
    include: [
      {
        model: GRNItem, // Include GRN items
        as: 'items', // Use items alias
        include: [
          {
            model: Product, // Include product
            as: 'product', // Use product alias
          },
        ],
      },
    ],
  });
  
  // Check if GRN exists
  if (!grn) {
    throw new NotFoundError(`GRN with ID ${grnId} not found`); // Throw error if not found
  }
  
  // Validate GRN has items
  if (!grn.items || grn.items.length === 0) {
    throw new ValidationError('GRN has no items to process'); // Throw error if no items
  }

  // Check if GRN is already processed
  if (grn.processed_at) {
    throw new ValidationError('GRN has already been processed'); // Throw error if already processed
  }
  
  // Check if any items are RM products requiring dimension-based processing
  const hasRMProducts = grn.items.some(item => 
    item.product.product_type === 'RM' && item.product.track_by_dimensions
  );
  
  if (hasRMProducts) {
    // Use dimension-based processing service
    logger.info(`Processing GRN ${grnId} with dimension-based RM products`);
    return await grnProcessingService.processGRNWithDimensions(grnId, userId);
  }
  
  // Fall back to original quantity-based processing for non-RM products
  logger.info(`Processing GRN ${grnId} with quantity-based products`);
  
  // Start database transaction
  const transaction = await sequelize.transaction(); // Start transaction
  
  try {
    // Process each GRN item
    for (const item of grn.items) {
      // Get product
      const product = await Product.findByPk(item.product_id, { transaction }); // Find product in transaction
      
      // **IMPORTANT**: UUID generation is ONLY for FG (Finished Goods) products
      // RM (Raw Materials) products should NOT get UUIDs - they are tracked by quantity/dimensions only
      // Both RM and FG products can be procured via GRN, but only FG products get UUIDs
      const shouldGenerateUIDs = generateUIDs && product.track_inventory && product.product_type === 'FG';
      
      // Generate UIDs if requested and product is FG type
      if (shouldGenerateUIDs) {
        // Generate inventory items with UIDs for each unit received
        const quantity = Math.floor(parseFloat(item.quantity)); // Get integer quantity (for UID-based items)
        
        for (let i = 0; i < quantity; i++) {
          // Generate UID and barcode
          let itemUID = generateUID(); // Generate UID
          const itemBarcode = generateBarcode(); // Generate barcode
          
          // Check if UID already exists (unlikely but possible)
          let existingItem = await InventoryItem.findOne({
            where: { uid: itemUID }, // Match UID
            transaction, // Use transaction
          });
          
          // Retry UID generation if it exists (unlikely but handle it)
          while (existingItem) {
            itemUID = generateUID(); // Generate new UID
            existingItem = await InventoryItem.findOne({
              where: { uid: itemUID }, // Match UID
              transaction, // Use transaction
            });
          }
          
          // Get or create quantity-based inventory record
          const [inventory] = await Inventory.findOrCreate({
            where: {
              product_id: item.product_id, // Match product ID
               // Match variant ID
            },
            defaults: {
              quantity: 0, // Default quantity
              reorder_level: 0, // Default reorder level
            },
            transaction, // Use transaction
          }); // Get or create inventory
          
          // Create inventory item with UID (within transaction)
          const inventoryItem = await InventoryItem.create({
            product_id: item.product_id, // Product ID
             // Variant ID
            uid: itemUID, // UID
            barcode: itemBarcode, // Barcode
            status: 'IN_STOCK', // Status: IN_STOCK
            source: 'GRN', // Source: GRN
            source_reference_id: grn.id, // Track which GRN created this item
          }, { transaction }); // Create inventory item in transaction
          
          // Increment quantity in quantity-based inventory (sync with UID-based)
          await inventory.increment('quantity', { by: 1, transaction }); // Increment quantity
          await inventory.reload({ transaction }); // Reload to get updated quantity
          
          // Create inventory movement for UID-based item
          await InventoryMovement.create({
            product_id: item.product_id, // Product ID
             // Variant ID
            inventory_item_id: inventoryItem.id, // Inventory item ID
            quantity_change: 1, // Quantity change (1 item)
            reason: 'GRN', // Reason: GRN
            reference_id: grn.id, // Reference ID: GRN ID
          }, { transaction }); // Create movement in transaction
        }
      } else {
        // If not generating UIDs, add stock directly to quantity-based inventory
        // Get or create quantity-based inventory record
        const [inventory] = await Inventory.findOrCreate({
          where: {
            product_id: item.product_id, // Match product ID
             // Match variant ID
          },
          defaults: {
            quantity: 0, // Default quantity
            reorder_level: 0, // Default reorder level
          },
          transaction, // Use transaction
        }); // Get or create inventory
        
        // Increment quantity
        await inventory.increment('quantity', { by: parseFloat(item.quantity), transaction }); // Increment quantity
        await inventory.reload({ transaction }); // Reload to get updated quantity
        
        // Create inventory movement for quantity-based adjustment
        await InventoryMovement.create({
          product_id: item.product_id, // Product ID
           // Variant ID
          inventory_item_id: null, // No inventory item for quantity-based
          quantity_change: parseFloat(item.quantity), // Quantity change
          reason: 'GRN', // Reason: GRN
          reference_id: grn.id, // Reference ID: GRN ID
        }, { transaction }); // Create movement in transaction
      }
    }
    
    // Update GRN with processed_at timestamp
    await grn.update(
      { processed_at: new Date() },
      { transaction }
    );
    
    // Commit transaction
    await transaction.commit(); // Commit transaction
    
    logger.info(`GRN processed: ${grnId} - Stock added to inventory`); // Log GRN processing
    
    // Return processed GRN
    return await getGRN(grnId); // Return GRN with associations
  } catch (error) {
    // Rollback transaction on error
    await transaction.rollback(); // Rollback transaction
    throw error; // Re-throw error
  }
};

// Export GRN service functions
module.exports = {
  createGRN,
  getGRN,
  listGRNs,
  processGRN,
};

