/**
 * Service class for Account business logic.
 * Provides account deduplication, enrichment, and territory assignment.
 * Delegates queries to AccountSelector and SObject manipulation to AccountDomain.
 */
public with sharing class AccountService {

    // ─── Constants ───────────────────────────────────────────────────────
    private static final String ERROR_NULL_INPUT = 'Input cannot be null or empty.';
    private static final String ERROR_MERGE_FAILED = 'Account merge failed for master Id: ';
    private static final Integer MAX_MERGE_BATCH = 3;

    // ─── Public API ──────────────────────────────────────────────────────

    /**
     * Merges duplicate Account records into a master record.
     * The master record retains its field values; child records are reparented.
     * @param masterIds Map of master Account Id to Set of duplicate Account Ids to merge
     * @return List of master Account Ids that were successfully merged
     * @throws AccountServiceException if merge processing fails
     * @example
     * Map<Id, Set<Id>> mergeMap = new Map<Id, Set<Id>>{
     *     masterAcctId => new Set<Id>{ dupeId1, dupeId2 }
     * };
     * List<Id> mergedIds = AccountService.mergeAccounts(mergeMap);
     */
    public static List<Id> mergeAccounts(Map<Id, Set<Id>> mergeMap) {
        if (mergeMap == null || mergeMap.isEmpty()) {
            throw new AccountServiceException(ERROR_NULL_INPUT);
        }

        // Collect all Ids for a single query
        Set<Id> allIds = new Set<Id>();
        allIds.addAll(mergeMap.keySet());
        for (Set<Id> dupeIds : mergeMap.values()) {
            allIds.addAll(dupeIds);
        }

        // Query all records at once via Selector
        Map<Id, Account> accountMap = AccountSelector.selectMapByIds(allIds);

        List<Id> successfulMerges = new List<Id>();
        List<String> errors = new List<String>();

        for (Id masterId : mergeMap.keySet()) {
            Account master = accountMap.get(masterId);
            if (master == null) {
                errors.add('Master account not found: ' + masterId);
                continue;
            }

            List<Account> duplicates = new List<Account>();
            for (Id dupeId : mergeMap.get(masterId)) {
                Account dupe = accountMap.get(dupeId);
                if (dupe != null) {
                    duplicates.add(dupe);
                }
            }

            try {
                // Apex merge supports up to 3 records at a time
                for (List<Account> chunk : chunkAccounts(duplicates, MAX_MERGE_BATCH)) {
                    Database.merge(master, chunk);
                }
                successfulMerges.add(masterId);
            } catch (DmlException e) {
                errors.add(ERROR_MERGE_FAILED + masterId + ' - ' + e.getMessage());
            }
        }

        if (!errors.isEmpty()) {
            System.debug(LoggingLevel.WARN, 'Merge errors: ' + String.join(errors, '\n'));
        }

        return successfulMerges;
    }

    /**
     * Assigns accounts to territories based on Billing State/Country.
     * Uses Custom Metadata Type (Territory_Mapping__mdt) for mappings.
     * @param accountIds Set of Account Ids to assign territories for
     * @return Number of accounts successfully updated
     * @throws AccountServiceException if territory assignment fails
     */
    public static Integer assignTerritories(Set<Id> accountIds) {
        if (accountIds == null || accountIds.isEmpty()) {
            throw new AccountServiceException(ERROR_NULL_INPUT);
        }

        List<Account> accounts = AccountSelector.selectWithBillingAddress(accountIds);
        Map<String, String> territoryMap = loadTerritoryMappings();

        List<Account> toUpdate = new List<Account>();
        for (Account acct : accounts) {
            String key = buildTerritoryKey(acct.BillingState, acct.BillingCountry);
            String territory = territoryMap.get(key);

            if (territory != null && territory != acct.Territory__c) {
                toUpdate.add(new Account(
                    Id = acct.Id,
                    Territory__c = territory
                ));
            }
        }

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

        return 0;
    }

    // ─── Convenience Overloads ───────────────────────────────────────────

    /**
     * Single-account territory assignment convenience method
     * @param accountId The Account Id to assign a territory for
     * @return 1 if updated, 0 if no change needed
     */
    public static Integer assignTerritory(Id accountId) {
        return assignTerritories(new Set<Id>{ accountId });
    }

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

    /**
     * Loads territory mappings from Custom Metadata
     * @return Map of territory key (State:Country) to territory name
     */
    private static Map<String, String> loadTerritoryMappings() {
        Map<String, String> mappings = new Map<String, String>();
        for (Territory_Mapping__mdt mapping : Territory_Mapping__mdt.getAll().values()) {
            String key = buildTerritoryKey(mapping.State__c, mapping.Country__c);
            mappings.put(key, mapping.Territory_Name__c);
        }
        return mappings;
    }

    /**
     * Builds a consistent territory lookup key
     * @param state The billing state
     * @param country The billing country
     * @return A normalized key string
     */
    private static String buildTerritoryKey(String state, String country) {
        return (state ?? '').toUpperCase() + ':' + (country ?? '').toUpperCase();
    }

    /**
     * Chunks a list of Accounts into sublists of the given size
     * @param accounts The accounts to chunk
     * @param chunkSize Maximum chunk size
     * @return List of account sublists
     */
    private static List<List<Account>> chunkAccounts(List<Account> accounts, Integer chunkSize) {
        List<List<Account>> chunks = new List<List<Account>>();
        List<Account> current = new List<Account>();

        for (Account acct : accounts) {
            current.add(acct);
            if (current.size() == chunkSize) {
                chunks.add(current);
                current = new List<Account>();
            }
        }
        if (!current.isEmpty()) {
            chunks.add(current);
        }
        return chunks;
    }

    /**
     * Counts successful results from a DML operation
     * @param results List of Database.SaveResult
     * @return Count of successful operations
     */
    private static Integer countSuccesses(List<Database.SaveResult> results) {
        Integer count = 0;
        for (Database.SaveResult sr : results) {
            if (sr.isSuccess()) {
                count++;
            } else {
                for (Database.Error err : sr.getErrors()) {
                    System.debug(LoggingLevel.WARN,
                        'Update failed for ' + sr.getId() + ': ' + err.getMessage()
                    );
                }
            }
        }
        return count;
    }

    // ─── Exception ───────────────────────────────────────────────────────

    /**
     * Custom exception for AccountService errors
     */
    public class AccountServiceException extends Exception {}
}
