/***********************************************************************************************************
 * Name         : SchemaUtils
 * Purpose      : Util class for Schema related methods
 **********************************************************************************************************/
public with sharing class SchemaUtils {
    public static final String USER_SOBJ_ID_PREFIX = '005';

    /**
     * Retrieves the three-character record prefix for a given SObject type.
     * The prefix is used as the first three characters of record IDs to identify the object type.
     *
     * @param objApiName The API name of the SObject (e.g., 'Account', 'Opportunity')
     * @return String The three-character record prefix for the object type
     */
    public static String getRecordPrefixByObjectApiName(String objApiName) {
        SObject instance = (SObject) Type.forName('Schema.' + objApiName).newInstance();
        return instance.getSObjectType().getDescribe().getKeyPrefix();
    }

    /**
     * Adds an SObject to a map organized by field values as keys.
     * If the key already exists in the map, appends the SObject to the existing list.
     * If the key is new, creates a new list with the SObject.
     *
     * @param stringSObjectListMap The map to be populated, keyed by field values
     * @param sObj The SObject to add to the map
     * @param fieldName The SObject field name whose value will be used as the map key
     */
    public static void addToFieldNameSObjectListMap(
        Map<String, List<SObject>> stringSObjectListMap,
        SObject sObj,
        String fieldName
    ) {
        String key = (String) String.valueOf(sObj.get(fieldName));
        if (stringSObjectListMap.containsKey(key)) {
            stringSObjectListMap.get(key).add(sObj);
        } else {
            stringSObjectListMap.put(key, new List<SObject>{ sObj });
        }
    }

    /**
     * Retrieves the FieldSetMembers for a specified FieldSet on a given SObject type.
     * FieldSets are groups of fields configured in the Salesforce UI for a specific object.
     *
     * @param sObjectTypeName The API name of the SObject type (e.g., 'Account', 'Opportunity')
     * @param fieldSetName The API name of the FieldSet to retrieve
     * @return List<Schema.FieldSetMember> List of FieldSetMembers, or null if the SObject or FieldSet is not found
     */
    public static List<Schema.FieldSetMember> getFieldSetMembers(String sObjectTypeName, String fieldSetName) {
        DescribeSObjectResult[] describes = Schema.describeSObjects(new List<String>{ sObjectTypeName });

        if (describes != null && describes.size() > 0) {
            return describes[0].fieldSets.getMap().get(fieldSetName).fields;
        } else {
            return null;
        }
    }

    /**
     * Converts a list of FieldSetMembers into a map keyed by field API name with user-facing labels as values.
     * Useful for building UI displays or field-to-label lookups from FieldSet definitions.
     *
     * @param fieldSetMembers List of FieldSetMember objects to convert
     * @return Map<String, String> Map of field API names to their labels
     */
    public static Map<String, String> getFieldSetAPINames(List<Schema.FieldSetMember> fieldSetMembers) {
        Map<String, String> fieldApiNamesToLabel = new Map<String, String>();

        for (Schema.FieldSetMember field : fieldSetMembers) {
            fieldApiNamesToLabel.put(field.getFieldPath(), field.getLabel());
        }

        return fieldApiNamesToLabel;
    }

    /**
     * Validates that changed fields are editable according to a provided set of allowed field names.
     * Compares new and old record values and identifies any fields that were modified but are not in the
     * editable fields list. These non-editable changed fields are added to the fieldsWithError list.
     *
     * @param allFields List of all SObjectField definitions for the object to check
     * @param editableFieldNames Set of field API names that are allowed to be edited
     * @param fieldsWithError List where non-editable fields that were changed will be added
     * @param newRecord The new record version with potential updates
     * @param oldRecord The old record version before updates
     */
    public static void checkEditableFields(
        List<Schema.SObjectField> allFields,
        Set<String> editableFieldNames,
        List<Schema.SObjectField> fieldsWithError,
        SObject newRecord,
        SObject oldRecord
    ) {
        for (Schema.SObjectField field : allFields) {
            String fieldName = field.getDescribe().getName();

            if (!editableFieldNames.contains(fieldName) && (newRecord.get(field) != oldRecord.get(field))) {
                fieldsWithError.add(field);
            }
        }
    }

    /**
     * Retrieves all field API names for a given SObject type.
     * Returns a list containing the API name of every field on the object.
     *
     * @param sObjectName The API name of the SObject (e.g., 'Account', 'Opportunity')
     * @return List<String> List of all field API names for the SObject
     */
    public static List<String> getAllFields(String sObjectName) {
        List<String> fields = new List<String>();
        Map<String, Schema.SObjectType> schemaMap = Schema.getGlobalDescribe();
        Map<String, Schema.SObjectField> fieldMap = schemaMap.get(sObjectName).getDescribe().fields.getMap();
        for (Schema.SObjectField field : fieldMap.values()) {
            DescribeFieldresult fieldDesc = field.getDescribe();
            fields.add(fieldDesc.getName());
        }
        return fields;
    }

    /**
     * Retrieves field descriptions (metadata) for all fields of a given SObject type.
     * Returns a map of field API names to their DescribeFieldResult objects, which contain detailed field metadata.
     *
     * @param sObjectName The API name of the SObject (e.g., 'Account', 'Opportunity')
     * @return Map<String, DescribeFieldResult> Map of field API names to their field descriptions
     */
    public static Map<String, DescribeFieldResult> getAllFieldsDescribe(String sObjectName) {
        return getAllFieldsDescribeBySObjectNames(new List<String>{ sObjectName }).get(sObjectName);
    }

    /**
     * Retrieves field descriptions (metadata) for all fields of multiple SObject types in a single call.
     * Returns a nested map structure with SObject names as keys and field description maps as values.
     * This is more efficient than calling getAllFieldsDescribe multiple times.
     *
     * @param sObjectNames List of SObject API names to retrieve field descriptions for
     * @return Map<String, Map<String, DescribeFieldResult>> Nested map: SObject name -> (field API name -> field description)
     */
    public static Map<String, Map<String, DescribeFieldResult>> getAllFieldsDescribeBySObjectNames(
        List<String> sObjectNames
    ) {
        Map<String, Map<String, DescribeFieldResult>> describeFieldsMap = new Map<String, Map<String, DescribeFieldResult>>();
        Map<String, Schema.SObjectType> schemaMap = Schema.getGlobalDescribe();

        for (String sObjectName : sObjectNames) {
            describeFieldsMap.put(sObjectName, new Map<String, DescribeFieldResult>());
            Map<String, Schema.SObjectField> fieldMap = schemaMap.get(sObjectName).getDescribe().fields.getMap();
            for (Schema.SObjectField field : fieldMap.values()) {
                DescribeFieldresult fieldDesc = field.getDescribe();
                describeFieldsMap.get(sObjectName).put(fieldDesc.getName(), fieldDesc);
            }
        }
        return describeFieldsMap;
    }

    /**
     * Retrieves all picklist fields and their available values for a given SObject type.
     * Includes both single-select picklist and multi-select picklist fields.
     * Returns a map where keys are field API names and values are lists of picklist entry values.
     *
     * @param objectApiName The API name of the SObject (e.g., 'Account', 'Opportunity')
     * @return Map<String, List<String>> Map of picklist field API names to their available values (entries)
     */
    public static Map<String, List<String>> getPicklistValuesMapByObjName(String objectApiName) {
        Map<String, List<String>> picklistMap = new Map<String, List<String>>();
        Map<String, Schema.SObjectType> globalDescribe = Schema.getGlobalDescribe();
        Schema.SObjectType objectType = globalDescribe.get(objectApiName);
        Schema.DescribeSObjectResult objectDescribe = objectType.getDescribe();

        for (Schema.SObjectField field : objectDescribe.fields.getMap().values()) {
            Schema.DescribeFieldResult fieldDescribe = field.getDescribe();
            if (
                fieldDescribe.getType() == Schema.DisplayType.PICKLIST ||
                fieldDescribe.getType() == Schema.DisplayType.MULTIPICKLIST
            ) {
                List<Schema.PicklistEntry> picklistValues = fieldDescribe.getPicklistValues();
                List<String> picklistLabels = new List<String>();

                for (Schema.PicklistEntry picklistEntry : picklistValues) {
                    picklistLabels.add(picklistEntry.getValue());
                }

                picklistMap.put(fieldDescribe.getName(), picklistLabels);
            }
        }
        return picklistMap;
    }

    /*
     * Populates a parent SObject's relationship property with a new related SObject instance.
     * Derives the lookup field ("__r" -> "__c"), finds the referenced SObject via describe,
     * instantiates it, sets provided field values, and assigns it to the relationship property.
     *
     * @param parent The parent SObject (any type)
     * @param relationshipName The relationship API name (e.g., 'ArtistOrLabelName__r')
     * @param fieldValues Map of field API names to values to set on the related sObject (e.g., {'Name' => 'John'})
     */
    public static void populateRelated(SObject parent, String relationshipName, Map<String, Object> fieldValues) {
        if (parent == null || String.isBlank(relationshipName)) {
            return;
        }
        String lookupField = relationshipName.endsWith('__r')
            ? relationshipName.replace('__r', '__c')
            : relationshipName;
        Map<String, Schema.SObjectField> fieldsMap = parent.getSObjectType().getDescribe().fields.getMap();
        SObject related;
        if (fieldsMap.containsKey(lookupField)) {
            Schema.DescribeFieldResult dfr = fieldsMap.get(lookupField).getDescribe();
            List<Schema.SObjectType> refs = dfr.getReferenceTo();
            if (!refs.isEmpty()) {
                Schema.SObjectType refType = refs[0];
                related = refType.newSObject();
            }
        }
        if (fieldValues != null) {
            for (String k : fieldValues.keySet()) {
                related.put(k, fieldValues.get(k));
            }
        }
        parent.putSObject(relationshipName, related);
    }

    /**
     * Retrieves a field value from an SObject, supporting both direct fields and dot-notation (e.g., 'Parent__r.Name').
     * Returns null if the SObject is null, field is blank, or the nested relationship is unavailable.
     *
     * @param sObj The SObject to retrieve the field value from
     * @param field The field API name (direct) or relationship path (dot-notation)
     * @return The field value, or null if not found
     */
    public static Object getFieldValueFromSObject(SObject sObj, String field) {
        Object result;
        if (sObj == null || String.isBlank(field)) {
            return result;
        }

        if (field.contains('.')) {
            List<String> composedName = field.split('\\.');
            if (composedName.size() == 2) {
                String relationshipName = composedName[0];
                String relationshipField = composedName[1];
                result = sObj.getSObject(relationshipName)?.get(relationshipField);
            }
        } else {
            result = sObj.get(field);
        }
        return result;
    }
}
