/**
 * @description Batch Apex class for identifying and flagging duplicate Account records.
 *              Compares Accounts by Name and BillingPostalCode to find potential duplicates.
 *              Flags duplicates by setting the Is_Potential_Duplicate__c checkbox.
 *              Implements Database.Stateful to track results across batch chunks.
 * @author Generated by Apex Class Writer Skill
 *
 * @example
 * // Run with default batch size (200)
 * Id jobId = AccountDeduplicationBatch.run();
 *
 * // Run with smaller batch size for complex processing
 * Database.executeBatch(new AccountDeduplicationBatch(), 50);
 */
public with sharing class AccountDeduplicationBatch implements Database.Batchable<SObject>, Database.Stateful {

    // ─── Constants ───────────────────────────────────────────────────────
    private static final Integer DEFAULT_BATCH_SIZE = 200;

    // ─── Stateful Tracking ───────────────────────────────────────────────
    private Integer totalScanned = 0;
    private Integer duplicatesFound = 0;
    private Integer totalErrors = 0;
    private List<String> errorMessages = new List<String>();

    // ─── Batchable Interface ─────────────────────────────────────────────

    /**
     * @description Queries all active Accounts that haven't already been flagged
     * @param bc The batch context
     * @return QueryLocator scoped to unflagged active Accounts
     */
    public Database.QueryLocator start(Database.BatchableContext bc) {
        return Database.getQueryLocator([
            SELECT Id, Name, BillingPostalCode, Is_Potential_Duplicate__c
            FROM Account
            WHERE IsDeleted = FALSE
            AND Is_Potential_Duplicate__c = FALSE
            ORDER BY Name ASC
        ]);
    }

    /**
     * @description Processes each batch by building a duplicate key and checking for matches.
     *              Uses a composite key of normalized Name + BillingPostalCode.
     * @param bc The batch context
     * @param scope List of Account records in the current batch
     */
    public void execute(Database.BatchableContext bc, List<Account> scope) {
        totalScanned += scope.size();

        // Build duplicate keys for this batch
        Map<String, List<Account>> dupeKeyMap = new Map<String, List<Account>>();
        for (Account acct : scope) {
            String key = buildDuplicateKey(acct);
            if (String.isNotBlank(key)) {
                if (!dupeKeyMap.containsKey(key)) {
                    dupeKeyMap.put(key, new List<Account>());
                }
                dupeKeyMap.get(key).add(acct);
            }
        }

        // Flag records that share a key with another record
        List<Account> toUpdate = new List<Account>();
        for (String key : dupeKeyMap.keySet()) {
            List<Account> group = dupeKeyMap.get(key);
            if (group.size() > 1) {
                for (Account acct : group) {
                    toUpdate.add(new Account(
                        Id = acct.Id,
                        Is_Potential_Duplicate__c = true
                    ));
                }
            }
        }

        if (!toUpdate.isEmpty()) {
            List<Database.SaveResult> results = Database.update(toUpdate, false);
            processResults(results);
        }
    }

    /**
     * @description Logs a summary of the deduplication batch run
     * @param bc The batch context
     */
    public void finish(Database.BatchableContext bc) {
        String summary = String.format(
            'AccountDeduplicationBatch completed. Scanned: {0}, Duplicates flagged: {1}, Errors: {2}',
            new List<Object>{ totalScanned, duplicatesFound, totalErrors }
        );

        System.debug(LoggingLevel.INFO, summary);

        if (!errorMessages.isEmpty()) {
            System.debug(LoggingLevel.ERROR, 'Error details:\n' + String.join(errorMessages, '\n'));
        }
    }

    // ─── Private Helpers ─────────────────────────────────────────────────

    /**
     * @description Builds a normalized composite key for duplicate detection
     * @param acct The Account record
     * @return Normalized key string, or null if insufficient data
     */
    private String buildDuplicateKey(Account acct) {
        if (String.isBlank(acct.Name)) {
            return null;
        }

        String normalizedName = acct.Name.trim().toUpperCase().replaceAll('\\s+', ' ');
        String postalCode = (acct.BillingPostalCode ?? '').trim().toUpperCase();

        return normalizedName + '|' + postalCode;
    }

    /**
     * @description Processes DML results, tracking successes and failures
     * @param results List of Database.SaveResult from update operation
     */
    private void processResults(List<Database.SaveResult> results) {
        for (Database.SaveResult sr : results) {
            if (sr.isSuccess()) {
                duplicatesFound++;
            } else {
                totalErrors++;
                for (Database.Error err : sr.getErrors()) {
                    errorMessages.add(
                        'Record ' + sr.getId() + ': ' +
                        err.getStatusCode() + ' - ' + err.getMessage()
                    );
                }
            }
        }
    }

    // ─── Static Helpers ──────────────────────────────────────────────────

    /**
     * @description Convenience method to execute with default batch size
     * @return The batch job Id
     */
    public static Id run() {
        return Database.executeBatch(new AccountDeduplicationBatch(), DEFAULT_BATCH_SIZE);
    }
}
