/**
 * Bulkified Query Patterns
 *
 * This file demonstrates the correct patterns for querying related data
 * in bulk operations (triggers, batches, etc.) without hitting governor limits.
 *
 * Key Pattern: Collect IDs first, query once, use Map for O(1) lookups.
 *
 * @see https://www.apexhours.com/bulkification-of-apex-triggers/
 * @see https://medium.com/@saurabh.samirs/salesforce-apex-triggers-5-bulkification-patterns-to-avoid-soql-dml-limits-f4e9c8bbfb3a
 */

// ═══════════════════════════════════════════════════════════════════════════
// PATTERN 1: Simple Map Lookup (Parent Record Access)
// ═══════════════════════════════════════════════════════════════════════════

/**
 * ❌ ANTI-PATTERN: Query inside loop
 * This code will fail with "Too many SOQL queries: 101" for 101+ records
 */
public class AntiPatternExample {
    public void processContacts_WRONG(List<Contact> contacts) {
        for (Contact c : contacts) {
            // ❌ QUERY IN LOOP - will hit governor limit!
            Account a = [SELECT Name FROM Account WHERE Id = :c.AccountId];
            c.Description = 'Account: ' + a.Name;
        }
    }
}

/**
 * ✅ CORRECT PATTERN: Bulk query with Map lookup
 * Uses 1 query regardless of record count
 */
public class BulkPatternExample {
    public void processContacts_CORRECT(List<Contact> contacts) {
        // Step 1: Collect all parent IDs
        Set<Id> accountIds = new Set<Id>();
        for (Contact c : contacts) {
            if (c.AccountId != null) {
                accountIds.add(c.AccountId);
            }
        }

        // Step 2: Single bulk query
        Map<Id, Account> accountMap = new Map<Id, Account>([
            SELECT Id, Name
            FROM Account
            WHERE Id IN :accountIds
            WITH USER_MODE
        ]);

        // Step 3: O(1) Map lookups
        for (Contact c : contacts) {
            Account a = accountMap.get(c.AccountId);
            if (a != null) {
                c.Description = 'Account: ' + a.Name;
            }
        }
    }
}

// ═══════════════════════════════════════════════════════════════════════════
// PATTERN 2: Grouped Child Records (One-to-Many)
// ═══════════════════════════════════════════════════════════════════════════

/**
 * Get child records grouped by parent ID
 * Useful when you need all children for each parent
 */
public class GroupedChildPattern {

    /**
     * Returns Contacts grouped by their AccountId
     */
    public static Map<Id, List<Contact>> getContactsByAccountId(Set<Id> accountIds) {
        Map<Id, List<Contact>> contactsByAccount = new Map<Id, List<Contact>>();

        // Initialize empty lists for each account
        for (Id accountId : accountIds) {
            contactsByAccount.put(accountId, new List<Contact>());
        }

        // Single query for all contacts
        for (Contact c : [
            SELECT Id, FirstName, LastName, Email, AccountId
            FROM Contact
            WHERE AccountId IN :accountIds
            WITH USER_MODE
        ]) {
            contactsByAccount.get(c.AccountId).add(c);
        }

        return contactsByAccount;
    }

    /**
     * Usage example in a trigger
     */
    public void onAccountUpdate(List<Account> accounts) {
        Set<Id> accountIds = new Set<Id>();
        for (Account a : accounts) {
            accountIds.add(a.Id);
        }

        // Get all contacts for all accounts in ONE query
        Map<Id, List<Contact>> contactsByAccount = getContactsByAccountId(accountIds);

        for (Account a : accounts) {
            List<Contact> accountContacts = contactsByAccount.get(a.Id);
            System.debug('Account ' + a.Name + ' has ' + accountContacts.size() + ' contacts');
        }
    }
}

// ═══════════════════════════════════════════════════════════════════════════
// PATTERN 3: Multi-Level Relationship Lookup
// ═══════════════════════════════════════════════════════════════════════════

/**
 * When you need to traverse multiple relationship levels
 * Example: Contact -> Account -> Owner
 */
public class MultiLevelLookupPattern {

    public void enrichContactsWithOwnerInfo(List<Contact> contacts) {
        // Step 1: Get Account IDs
        Set<Id> accountIds = new Set<Id>();
        for (Contact c : contacts) {
            if (c.AccountId != null) {
                accountIds.add(c.AccountId);
            }
        }

        // Step 2: Query Accounts with Owner info (single query)
        Map<Id, Account> accountMap = new Map<Id, Account>([
            SELECT Id, Name, OwnerId, Owner.Name, Owner.Email
            FROM Account
            WHERE Id IN :accountIds
            WITH USER_MODE
        ]);

        // Step 3: Use the data
        for (Contact c : contacts) {
            Account a = accountMap.get(c.AccountId);
            if (a != null && a.Owner != null) {
                c.Description = 'Account Owner: ' + a.Owner.Name;
            }
        }
    }
}

// ═══════════════════════════════════════════════════════════════════════════
// PATTERN 4: Conditional Query Based on Field Values
// ═══════════════════════════════════════════════════════════════════════════

/**
 * When you only need to query for certain records based on field values
 */
public class ConditionalQueryPattern {

    public void updateHighValueOpportunities(List<Opportunity> opportunities) {
        // Step 1: Filter to high-value opportunities only
        Set<Id> highValueAccountIds = new Set<Id>();
        for (Opportunity opp : opportunities) {
            if (opp.Amount != null && opp.Amount > 100000 && opp.AccountId != null) {
                highValueAccountIds.add(opp.AccountId);
            }
        }

        // Step 2: Only query if we have high-value records
        if (highValueAccountIds.isEmpty()) {
            return; // No query needed!
        }

        Map<Id, Account> accountMap = new Map<Id, Account>([
            SELECT Id, Name, AnnualRevenue, Industry
            FROM Account
            WHERE Id IN :highValueAccountIds
            WITH USER_MODE
        ]);

        // Step 3: Process only the high-value opportunities
        for (Opportunity opp : opportunities) {
            if (opp.Amount != null && opp.Amount > 100000) {
                Account a = accountMap.get(opp.AccountId);
                if (a != null) {
                    opp.Description = 'High-value opp for ' + a.Industry + ' account';
                }
            }
        }
    }
}

// ═══════════════════════════════════════════════════════════════════════════
// PATTERN 5: Avoiding Duplicate Queries in Complex Logic
// ═══════════════════════════════════════════════════════════════════════════

/**
 * When the same data is needed in multiple methods,
 * query once and pass the Map around
 */
public class SharedQueryPattern {

    private Map<Id, Account> accountCache;

    /**
     * Initialize the cache once at the start of processing
     */
    public void initializeCache(Set<Id> accountIds) {
        this.accountCache = new Map<Id, Account>([
            SELECT Id, Name, Industry, AnnualRevenue, OwnerId
            FROM Account
            WHERE Id IN :accountIds
            WITH USER_MODE
        ]);
    }

    /**
     * Multiple methods can use the cached data
     */
    public String getAccountName(Id accountId) {
        Account a = accountCache.get(accountId);
        return a != null ? a.Name : null;
    }

    public String getAccountIndustry(Id accountId) {
        Account a = accountCache.get(accountId);
        return a != null ? a.Industry : null;
    }

    public Decimal getAccountRevenue(Id accountId) {
        Account a = accountCache.get(accountId);
        return a != null ? a.AnnualRevenue : 0;
    }
}

// ═══════════════════════════════════════════════════════════════════════════
// PATTERN 6: SOQL For Loop (Large Dataset Processing)
// ═══════════════════════════════════════════════════════════════════════════

/**
 * For processing very large datasets without hitting heap limits
 * The SOQL for loop fetches 200 records at a time
 */
public class LargeDatasetPattern {

    public void processAllAccounts() {
        // This fetches 200 records at a time, not all 50,000
        for (Account a : [
            SELECT Id, Name, Industry
            FROM Account
            WHERE CreatedDate = THIS_YEAR
        ]) {
            // Process one record at a time
            // Heap stays small because only 200 records in memory
            processAccount(a);
        }
    }

    public void processAllAccountsInBatches() {
        // Alternative: explicit batch processing (200 at a time)
        for (List<Account> accountBatch : [
            SELECT Id, Name, Industry
            FROM Account
            WHERE CreatedDate = THIS_YEAR
        ]) {
            // Process 200 records at a time
            processBatch(accountBatch);
        }
    }

    private void processAccount(Account a) {
        // Individual record processing
    }

    private void processBatch(List<Account> accounts) {
        // Batch processing
    }
}
