create or replace PACKAGE BODY CT_MRDS.ENV_MANAGER
AS
  
   ----------------------------------------------------------------------------------------------------

   PROCEDURE INIT_ERRORS IS
   BEGIN
      Errors(CODE_EMPTY_FILEURI_AND_RECKEY)        := Error_Record(CODE_EMPTY_FILEURI_AND_RECKEY,           MSG_EMPTY_FILEURI_AND_RECKEY);         --  -20001
      Errors(CODE_NO_CONFIG_MATCH_FOR_FILEURI)     := Error_Record(CODE_NO_CONFIG_MATCH_FOR_FILEURI,        MSG_NO_CONFIG_MATCH_FOR_FILEURI);      --  -20002
      Errors(CODE_MULTIPLE_MATCH_FOR_SRCFILE)      := Error_Record(CODE_MULTIPLE_MATCH_FOR_SRCFILE,         MSG_MULTIPLE_MATCH_FOR_SRCFILE);       --  -20003
      Errors(CODE_MISSING_COLUMN_DATE_FORMAT)      := Error_Record(CODE_MISSING_COLUMN_DATE_FORMAT,         MSG_MISSING_COLUMN_DATE_FORMAT);       --  -20004
      Errors(CODE_MULTIPLE_COLUMN_DATE_FORMAT)     := Error_Record(CODE_MULTIPLE_COLUMN_DATE_FORMAT,        MSG_MULTIPLE_COLUMN_DATE_FORMAT);      --  -20005
      Errors(CODE_DIDNT_GET_LOAD_OPERATION_ID)     := Error_Record(CODE_DIDNT_GET_LOAD_OPERATION_ID,        MSG_DIDNT_GET_LOAD_OPERATION_ID);      --  -20006
      Errors(CODE_NO_CONFIG_FOR_RECEIVED_FILE)     := Error_Record(CODE_NO_CONFIG_FOR_RECEIVED_FILE,        MSG_NO_CONFIG_FOR_RECEIVED_FILE);      --  -20007
      Errors(CODE_MULTI_CONFIG_FOR_RECEIVED_FILE)  := Error_Record(CODE_MULTI_CONFIG_FOR_RECEIVED_FILE,     MSG_MULTI_CONFIG_FOR_RECEIVED_FILE);   --  -20008
      Errors(CODE_FILE_NOT_FOUND_ON_CLOUD)         := Error_Record(CODE_FILE_NOT_FOUND_ON_CLOUD,            MSG_FILE_NOT_FOUND_ON_CLOUD);          --  -20009
      Errors(CODE_FILE_VALIDATION_FAILED)          := Error_Record(CODE_FILE_VALIDATION_FAILED,             MSG_FILE_VALIDATION_FAILED);           --  -20010
      Errors(CODE_EXCESS_COLUMNS_DETECTED)         := Error_Record(CODE_EXCESS_COLUMNS_DETECTED,            MSG_EXCESS_COLUMNS_DETECTED);          --  -20011
      Errors(CODE_NO_CONFIG_MATCH)                 := Error_Record(CODE_NO_CONFIG_MATCH,                    MSG_NO_CONFIG_MATCH);                  --  -20012
      Errors(CODE_UNKNOWN_PREFIX)                  := Error_Record(CODE_UNKNOWN_PREFIX,                     MSG_UNKNOWN_PREFIX);                   --  -20013
      Errors(CODE_TABLE_NOT_EXISTS)                := Error_Record(CODE_TABLE_NOT_EXISTS,                   MSG_TABLE_NOT_EXISTS);                 --  -20014
      Errors(CODE_COLUMN_NOT_EXISTS)               := Error_Record(CODE_COLUMN_NOT_EXISTS,                  MSG_COLUMN_NOT_EXISTS);                --  -20015
      Errors(CODE_UNSUPPORTED_DATA_TYPE)           := Error_Record(CODE_UNSUPPORTED_DATA_TYPE,              MSG_UNSUPPORTED_DATA_TYPE);            --  -20016
      Errors(CODE_MISSING_SOURCE_KEY)              := Error_Record(CODE_MISSING_SOURCE_KEY,                 MSG_MISSING_SOURCE_KEY);               --  -20017
      Errors(CODE_NULL_SOURCE_FILE_CONFIG_KEY)     := Error_Record(CODE_NULL_SOURCE_FILE_CONFIG_KEY,        MSG_NULL_SOURCE_FILE_CONFIG_KEY);      --  -20018
      Errors(CODE_DUPLICATED_SOURCE_KEY)           := Error_Record(CODE_DUPLICATED_SOURCE_KEY,              MSG_DUPLICATED_SOURCE_KEY);            --  -20019
      Errors(CODE_MISSING_CONTAINER_CONFIG)        := Error_Record(CODE_MISSING_CONTAINER_CONFIG,           MSG_MISSING_CONTAINER_CONFIG);         --  -20020
      Errors(CODE_MULTIPLE_CONTAINER_ENTRIES)      := Error_Record(CODE_MULTIPLE_CONTAINER_ENTRIES,         MSG_MULTIPLE_CONTAINER_ENTRIES);       --  -20021
      Errors(CODE_WRONG_DESTINATION_PARAM)         := Error_Record(CODE_WRONG_DESTINATION_PARAM,            MSG_WRONG_DESTINATION_PARAM);          --  -20022
      Errors(CODE_FILE_NOT_EXISTS_ON_CLOUD)        := Error_Record(CODE_FILE_NOT_EXISTS_ON_CLOUD,           MSG_FILE_NOT_EXISTS_ON_CLOUD);         --  -20023
      Errors(CODE_FILE_ALREADY_REGISTERED)         := Error_Record(CODE_FILE_ALREADY_REGISTERED,            MSG_FILE_ALREADY_REGISTERED);          --  -20024
      Errors(CODE_WRONG_DATE_TIMESTAMP_FORMAT)     := Error_Record(CODE_WRONG_DATE_TIMESTAMP_FORMAT,        MSG_WRONG_DATE_TIMESTAMP_FORMAT);      --  -20025
      Errors(CODE_ENVIRONMENT_NOT_SET)             := Error_Record(CODE_ENVIRONMENT_NOT_SET,                MSG_ENVIRONMENT_NOT_SET);              --  -20026
      Errors(CODE_CONFIG_VARIABLE_NOT_SET)         := Error_Record(CODE_CONFIG_VARIABLE_NOT_SET,            MSG_CONFIG_VARIABLE_NOT_SET);          --  -20027
      Errors(CODE_NOT_INPUT_SOURCE_FILE_TYPE)      := Error_Record(CODE_NOT_INPUT_SOURCE_FILE_TYPE,         MSG_NOT_INPUT_SOURCE_FILE_TYPE);       --  -20028
      Errors(CODE_EXP_DATA_FOR_ARCH_FAILED)        := Error_Record(CODE_EXP_DATA_FOR_ARCH_FAILED,           MSG_EXP_DATA_FOR_ARCH_FAILED);         --  -20029
      Errors(CODE_RESTORE_FILE_FROM_TRASH)         := Error_Record(CODE_RESTORE_FILE_FROM_TRASH,            MSG_RESTORE_FILE_FROM_TRASH);          --  -20030
      Errors(CODE_CHANGE_STAT_TO_ARCHIVED_FAILED):= Error_Record(CODE_CHANGE_STAT_TO_ARCHIVED_FAILED,   MSG_CHANGE_STAT_TO_ARCHIVED_FAILED); --  -20031
      Errors(CODE_MOVE_FILE_TO_TRASH_FAILED)       := Error_Record(CODE_MOVE_FILE_TO_TRASH_FAILED,          MSG_MOVE_FILE_TO_TRASH_FAILED);        --  -20032
      Errors(CODE_DROP_EXPORTED_FILES_FAILED)      := Error_Record(CODE_DROP_EXPORTED_FILES_FAILED,         MSG_DROP_EXPORTED_FILES_FAILED);       --  -20033
      Errors(CODE_INVALID_BUCKET_AREA)             := Error_Record(CODE_INVALID_BUCKET_AREA,                MSG_INVALID_BUCKET_AREA);              --  -20034
      Errors(CODE_INVALID_PARALLEL_DEGREE)         := Error_Record(CODE_INVALID_PARALLEL_DEGREE,            MSG_INVALID_PARALLEL_DEGREE);          --  -20110
      Errors(CODE_PARALLEL_EXECUTION_FAILED)       := Error_Record(CODE_PARALLEL_EXECUTION_FAILED,          MSG_PARALLEL_EXECUTION_FAILED);        --  -20111

      Errors(CODE_UNKNOWN)                         := Error_Record(CODE_UNKNOWN,                         MSG_UNKNOWN);                          --  -20999

   END INIT_ERRORS;

   ----------------------------------------------------------------------------------------------------

   FUNCTION GET_DEFAULT_ENV 
   RETURN VARCHAR2
   IS
      vDefaultEnv CT_MRDS.a_file_manager_config.config_variable_value%TYPE;
   BEGIN
      select config_variable_value 
        into vDefaultEnv 
        from CT_MRDS.a_file_manager_config 
       where lower(environment_id)='default'
         and lower(config_variable)='environmentid';
      RETURN vDefaultEnv;
   EXCEPTION 
      WHEN NO_DATA_FOUND THEN
         RETURN NULL;
   END;

   ----------------------------------------------------------------------------------------------------

   ----------------------------------------------------------------------------------------------------

   PROCEDURE INIT_VARIABLES(
      pEnv VARCHAR2
   ) IS
   BEGIN
      for rec in (
         select 
             ENVIRONMENT_ID
            ,REGION
            ,NAMESPACE
            ,INBOXBUCKETNAME
            ,DATABUCKETNAME
            ,ARCHIVEBUCKETNAME
            ,CREDENTIALNAME
            ,LOGGINGENABLED
            ,MINLOGLEVEL
            ,DEFAULTDATEFORMAT
            ,CONSOLELOGGINGENABLED
         from (
            select environment_id, config_variable, config_variable_value from CT_MRDS.A_FILE_MANAGER_CONFIG 
            where environment_id=pEnv
            )
         pivot (
            min(config_variable_value) 
            for config_variable in ( 
                'Region'                as Region
               ,'NameSpace'             as NameSpace
               ,'InboxBucketName'       as InboxBucketName
               ,'DataBucketName'        as DataBucketName
               ,'ArchiveBucketName'     as ArchiveBucketName
               ,'CredentialName'        as CredentialName
               ,'LoggingEnabled'        as LoggingEnabled
               ,'MinLogLevel'           as MinLogLevel
               ,'DefaultDateFormat'     as DefaultDateFormat
               ,'ConsoleLoggingEnabled' as ConsoleLoggingEnabled)
            )
         ) loop
            if   (rec.NAMESPACE            is NULL
               or rec.REGION               is NULL
               or rec.NAMESPACE            is NULL
               or rec.INBOXBUCKETNAME      is NULL
               or rec.DATABUCKETNAME       is NULL
               or rec.ARCHIVEBUCKETNAME    is NULL
               or rec.CREDENTIALNAME       is NULL
            ) THEN
               vgMsgTmp := MSG_CONFIG_VARIABLE_NOT_SET
                     ||cgBL||'           '||'Details about existing Configuration Variables where environment_id='||pEnv||': '
                     ||cgBL||'           '||'-------------------------'
                     ||cgBL||'           '||'Region =            '||rec.Region
                     ||cgBL||'           '||'NameSpace =         '||rec.NameSpace
                     ||cgBL||'           '||'InboxBucketName =   '||rec.InboxBucketName
                     ||cgBL||'           '||'DataBucketName =    '||rec.DataBucketName
                     ||cgBL||'           '||'ArchiveBucketName = '||rec.ArchiveBucketName
                     ||cgBL||'           '||'CredentialName =    '||rec.CredentialName
                     ;
               LOG_PROCESS_EVENT(vgMsgTmp, 'ERROR');
               RAISE_APPLICATION_ERROR(CODE_CONFIG_VARIABLE_NOT_SET, vgMsgTmp);

            elsif (rec.LOGGINGENABLED      is NULL
               or rec.MINLOGLEVEL          is NULL
               or rec.DEFAULTDATEFORMAT    is NULL
            ) THEN
               vgMsgTmp := 'Missing configuration variables'
                     ||cgBL||'           '||'Details about existing Configuration Variables where environment_id='||pEnv||': '
                     ||cgBL||'           '||'-------------------------'
                     ||cgBL||'           '||'LoggingEnabled =    '||rec.LoggingEnabled
                     ||cgBL||'           '||'MinLogLevel =       '||rec.MinLogLevel
                     ||cgBL||'           '||'DefaultDateFormat = '||rec.DefaultDateFormat
                     ;
               LOG_PROCESS_EVENT(vgMsgTmp, 'WARNING');

            else
               gvNameSpace          := rec.NAMESPACE;
               gvRegion             := rec.REGION;
               gvInboxBucketName    := rec.INBOXBUCKETNAME;
               gvDataBucketName     := rec.DATABUCKETNAME;
               gvArchiveBucketName  := rec.ARCHIVEBUCKETNAME;
               gvCredentialName     := rec.CREDENTIALNAME;
               gvInboxBucketUri     := 'https://objectstorage.'||rec.REGION||'.oraclecloud.com/n/'||rec.NAMESPACE||'/b/'||rec.INBOXBUCKETNAME||'/o/';  
               gvDataBucketUri      := 'https://objectstorage.'||rec.REGION||'.oraclecloud.com/n/'||rec.NAMESPACE||'/b/'||rec.DATABUCKETNAME||'/o/';  
               gvArchiveBucketUri   := 'https://objectstorage.'||rec.REGION||'.oraclecloud.com/n/'||rec.NAMESPACE||'/b/'||rec.ARCHIVEBUCKETNAME||'/o/';  
               gvLoggingEnabled     := rec.LOGGINGENABLED;
               gvMinLogLevel        := rec.MINLOGLEVEL;
               gvDefaultDateFormat  := rec.DEFAULTDATEFORMAT;
               gvConsoleLoggingEnabled := NVL(rec.CONSOLELOGGINGENABLED, 'ON');
            end if;
      end loop;
      EXCEPTION
         WHEN NO_DATA_FOUND THEN
            vgMsgTmp := MSG_CONFIG_VARIABLE_NOT_SET
                  ||cgBL||'           '||'No configuration found for environment_id='||pEnv||' in A_FILE_MANAGER_CONFIG table';
            LOG_PROCESS_EVENT(vgMsgTmp, 'ERROR', 'pEnv='||pEnv);
            RAISE_APPLICATION_ERROR(CODE_CONFIG_VARIABLE_NOT_SET, vgMsgTmp);
         WHEN OTHERS THEN
            vgMsgTmp := 'Unexpected error while initializing variables for environment: '||pEnv
                  ||cgBL||'           '||'SQLERRM: '||SQLERRM;
            LOG_PROCESS_EVENT(vgMsgTmp, 'ERROR', 'pEnv='||pEnv);
            RAISE;
   END INIT_VARIABLES;

   ----------------------------------------------------------------------------------------------------

   FUNCTION GET_ERROR_MESSAGE(
      pCode PLS_INTEGER
   ) RETURN VARCHAR2
   IS
   BEGIN
      RETURN Errors(pCode).message;
   EXCEPTION
      WHEN NO_DATA_FOUND THEN
         LOG_PROCESS_EVENT('No error message found for pCode='||pCode , 'WARNING', 'pCode='||pCode);
         LOG_PROCESS_EVENT('Update ENV_MANAGER package header with new code.' , 'WARNING', 'pCode='||pCode);
         RETURN NULL;
      WHEN OTHERS THEN
         LOG_PROCESS_EVENT(MSG_UNKNOWN , 'ERROR', 'pCode='||pCode);
         RAISE_APPLICATION_ERROR(CODE_UNKNOWN, MSG_UNKNOWN);
   END GET_ERROR_MESSAGE;

   ----------------------------------------------------------------------------------------------------

   FUNCTION GET_ERROR_STACK(
      pFormat  VARCHAR2
     ,pCode    PLS_INTEGER
     ,pSourceFileReceivedKey CT_MRDS.A_SOURCE_FILE_RECEIVED.A_SOURCE_FILE_RECEIVED_KEY%TYPE DEFAULT NULL
   ) RETURN VARCHAR2
   IS
      vFullErrorCore VARCHAR2(32000);
      vFullErrorMsg  VARCHAR2(32000);
   BEGIN
--      vgErrorMessage    := SQLERRM|| cgBL;
--      vgErrorStack      := DBMS_UTILITY.FORMAT_ERROR_STACK;
--      vgErrorBacktrace  := DBMS_UTILITY.FORMAT_ERROR_BACKTRACE;
      vFullErrorCore :='Error Message:'
                     ||cgBL|| SQLERRM|| cgBL
                     ||'-------------------------------------------------------'
                     ||cgBL||'Error Stack:'
                     ||cgBL|| DBMS_UTILITY.FORMAT_ERROR_STACK
                     ||'-------------------------------------------------------'
                     ||cgBL||'Error Backtrace:'
                     ||cgBL|| DBMS_UTILITY.FORMAT_ERROR_BACKTRACE;
--      vFullErrorCore := REGEXP_REPLACE (vFullErrorCore,   pCode||': ', pCode||': '||GET_ERROR_MESSAGE(pCode) ,  1, 1);
      IF (pFormat = 'TABLE') THEN
         vFullErrorMsg := vFullErrorCore;
      ELSE
         vFullErrorMsg := cgBL||'------------------------------------------------------+'
                        ||cgBL||vFullErrorCore
                              ||'------------------------------------------------------+';
      END IF;
--      IF pSourceFileReceivedKey is not null THEN
--         vFullErrorMsg := vFullErrorMsg ||cgBL||GET_DET_SOURCE_FILE_RECEIVED_INFO(pSourceFileReceivedKey,1,1,1);
--      END IF;

      RETURN vFullErrorMsg;
   EXCEPTION
      WHEN OTHERS THEN
         LOG_PROCESS_EVENT(MSG_UNKNOWN , 'ERROR', 'pFormat='||pFormat);
         RETURN NULL;
   END GET_ERROR_STACK;

   ----------------------------------------------------------------------------------------------------

   FUNCTION FORMAT_PARAMETERS(
      pParameterList SYS.ODCIVARCHAR2LIST
   ) RETURN VARCHAR2 IS
      vResult VARCHAR2(10000);
   BEGIN
      FOR i IN 1 .. pParameterList.COUNT LOOP
--         dbms_output.put_line('pParameterList(i): '||pParameterList(i));
         if i < pParameterList.COUNT then vResult := vResult || replace(pParameterList(i), '''NULL''', 'NULL') ||' ,'|| cgBL;
                                     else vResult := vResult || replace(pParameterList(i), '''NULL''', 'NULL'); 
         end if;
      END LOOP;
      RETURN vResult;
   EXCEPTION
      WHEN OTHERS THEN
         LOG_PROCESS_EVENT('Error while formating parameters.' , 'WARNING');
         RETURN NULL;
   END FORMAT_PARAMETERS;

   ----------------------------------------------------------------------------------------------------



   PROCEDURE LOG_PROCESS_EVENT (
       pLogMessage   VARCHAR2
      ,pLogLevel     VARCHAR2 DEFAULT 'ERROR'
      ,pParameters   VARCHAR2 DEFAULT NULL
      ,pProcessName  VARCHAR2 DEFAULT 'FILE_MANAGER'
   ) IS
   PRAGMA AUTONOMOUS_TRANSACTION;

      vLoggingEnabled   VARCHAR2(10);
      vMinLogLevel      VARCHAR2(10);
      vCallStack        VARCHAR2(10000);
      vProcedureName    VARCHAR2(100);
      vProcedureLevel   PLS_INTEGER;
      vTotalLines       PLS_INTEGER;
      vCurrentLine      PLS_INTEGER;

      -- Map of priority level
      TYPE logLevelMap IS TABLE OF NUMBER INDEX BY VARCHAR2(10);
      vLogLevels logLevelMap;

   BEGIN
      -- Prority logging level (higher -> more important)
      vLogLevels('DEBUG')   := 1;
      vLogLevels('INFO')    := 2;
      vLogLevels('WARNING') := 3;
      vLogLevels('ERROR')   := 4;

      -- Check id logging is TURN-OFF
      IF gvLoggingEnabled = 'OFF' THEN
         RETURN;
      END IF;
      -- Check logging level
      IF vLogLevels(pLogLevel) < vLogLevels(gvMinLogLevel) THEN
         RETURN;
      END IF;

      vCallStack := DBMS_UTILITY.FORMAT_CALL_STACK;
      vProcedureName := REGEXP_SUBSTR(vCallStack, 'package body\s+\w+\.(\w+\.\w+)', 1, 2, NULL, 1);
      vTotalLines := REGEXP_COUNT(vCallStack, CHR(10)) + 1;
      vCurrentLine := REGEXP_COUNT(SUBSTR(vCallStack, 1, INSTR(vCallStack, vProcedureName) - 1), CHR(10)) + 1;
      vProcedureLevel := (vTotalLines - vCurrentLine + 1) - 3;
      vProcedureName := LPAD(vProcedureName, LENGTH(vProcedureName) + 2*vProcedureLevel, ' ');

      INSERT INTO CT_MRDS.A_PROCESS_LOG (guid, username, osuser, machine, module, process_name, procedure_name, procedure_parameters, log_level, log_message)
      VALUES (guid, gvUsername, gvOsuser, gvMachine, gvModule, pProcessName, vProcedureName, pParameters, pLogLevel, pLogMessage);

      COMMIT;

      -- Also output to console for immediate visibility (if enabled)
      IF gvConsoleLoggingEnabled = 'ON' THEN
         DBMS_OUTPUT.PUT_LINE('[' || TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS') || '] [' || pLogLevel || '] ' || vProcedureName || ': ' || pLogMessage);
      END IF;

   END LOG_PROCESS_EVENT;

   ----------------------------------------------------------------------------------------------------

   PROCEDURE LOG_PROCESS_ERROR(
      pLogMessage IN VARCHAR2,
      pParameters IN VARCHAR2 DEFAULT NULL,
      pProcessName IN VARCHAR2 DEFAULT 'FILE_MANAGER'
   ) IS
   PRAGMA AUTONOMOUS_TRANSACTION;

      vCallStack        VARCHAR2(32767);
      vErrorStack       VARCHAR2(32767);
      vErrorBacktrace   VARCHAR2(32767);
      vAdjustedBacktrace VARCHAR2(32767);
      vErrorContext     VARCHAR2(4000);
      vProcName         VARCHAR2(100);
      vProcedureLevel   PLS_INTEGER;
      vTotalLines       PLS_INTEGER;
      vCurrentLine      PLS_INTEGER;
      vFullErrorMessage CLOB;
      vTimestamp        VARCHAR2(30);
      vSessionInfo      VARCHAR2(1000);

   BEGIN
      -- Check if logging is disabled
      IF gvLoggingEnabled = 'OFF' THEN
         RETURN;
      END IF;

      -- Capture all available error information
      vErrorStack := DBMS_UTILITY.FORMAT_ERROR_STACK;
      vErrorBacktrace := DBMS_UTILITY.FORMAT_ERROR_BACKTRACE;
      vCallStack := DBMS_UTILITY.FORMAT_CALL_STACK;
      vTimestamp := TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS');

      -- Capture session information for better context
      vSessionInfo := 'Session ID: ' || SYS_CONTEXT('USERENV', 'SID') ||
                     ', User: ' || SYS_CONTEXT('USERENV', 'SESSION_USER') ||
                     ', Module: ' || SYS_CONTEXT('USERENV', 'MODULE') ||
                     ', Client Info: ' || NVL(SYS_CONTEXT('USERENV', 'CLIENT_INFO'), 'N/A') ||
                     ', Action: ' || NVL(SYS_CONTEXT('USERENV', 'ACTION'), 'N/A');

      -- Build error context information
      vErrorContext := 'Environment: ' || gvEnv || 
                      ', Process: ' || NVL(pProcessName, 'UNKNOWN') ||
                      ', Timestamp: ' || vTimestamp ||
                      ', SQLCODE: ' || SQLCODE ||
                      ', Transaction Active: ' || CASE WHEN DBMS_TRANSACTION.STEP_ID IS NOT NULL THEN 'YES' ELSE 'NO' END;

      -- Extract procedure name and nesting level from call stack
      -- Always extract actual procedure name from call stack for precise error location
      vProcName := REGEXP_SUBSTR(vCallStack, 'package body\s+\w+\.(\w+\.\w+)', 1, 2, NULL, 1);

      -- If we couldn't extract procedure name from call stack, use provided process name
      IF vProcName IS NULL THEN
         vProcName := NVL(pProcessName, 'UNKNOWN');
      END IF;

      vTotalLines := REGEXP_COUNT(vCallStack, CHR(10)) + 1;
      vCurrentLine := REGEXP_COUNT(SUBSTR(vCallStack, 1, INSTR(vCallStack, vProcName) - 1), CHR(10)) + 1;
      vProcedureLevel := (vTotalLines - vCurrentLine + 1) - 3;
      vProcName := LPAD(vProcName, LENGTH(vProcName) + 2*vProcedureLevel, ' ');

      -- Enhance line number display to show direct _BODY.sql file line numbers
      -- Since packages are now split into separate _SPEC and _BODY files, line numbers map directly
      vAdjustedBacktrace := REGEXP_REPLACE(vErrorBacktrace, 
                                          'at "CT_MRDS\.FILE_MANAGER", line ([0-9]+)', 
                                          'at "CT_MRDS.FILE_MANAGER", line \1 (-> FILE_MANAGER_BODY.sql:line \1)', 1, 0, 'i');

      vAdjustedBacktrace := REGEXP_REPLACE(vAdjustedBacktrace, 
                                          'at "CT_MRDS\.ENV_MANAGER", line ([0-9]+)', 
                                          'at "CT_MRDS.ENV_MANAGER", line \1 (-> ENV_MANAGER_BODY.sql:line \1)', 1, 0, 'i');

      -- Build comprehensive error message with professional formatting
      vFullErrorMessage := 'ERROR REPORT' || cgBL ||
                          '-------------------------------------------------------' || cgBL ||
                          'ERROR SUMMARY' || cgBL ||
                          '   Message: ' || pLogMessage || cgBL ||
                          '   Context: ' || vErrorContext || cgBL ||
                          '-------------------------------------------------------' || cgBL ||
                          'SESSION INFORMATION' || cgBL ||
                          '   ' || vSessionInfo || cgBL ||
                          '-------------------------------------------------------' || cgBL ||
                          'ERROR STACK (Oracle Internal)' || cgBL ||
                          vErrorStack ||
                          '-------------------------------------------------------' || cgBL ||
                          'BACKTRACE INFORMATION (Oracle Internal)' || cgBL ||
                          vErrorBacktrace ||
                          '-------------------------------------------------------' || cgBL ||
                          'CALL STACK (Execution Path)' || cgBL ||
                          vCallStack ||
                          '-------------------------------------------------------' || cgBL ||
                          'QUICK REFERENCE' || cgBL ||
                          '   SQLCODE: ' || SQLCODE || cgBL ||
                          '   SQLERRM: ' || SQLERRM || cgBL ||
                          '   Timestamp: ' || vTimestamp || cgBL ||
                          '   Parameters: ' || NVL(pParameters, 'None provided') || cgBL ||
                          '-------------------------------------------------------';

      -- Insert comprehensive error record into log table
      -- Note: LOG_MESSAGE is VARCHAR2(4000), so we'll truncate if needed but include key info
      INSERT INTO CT_MRDS.A_PROCESS_LOG (guid, username, osuser, machine, module, process_name, procedure_name, procedure_parameters, log_level, log_message)
      VALUES (guid, gvUsername, gvOsuser, gvMachine, gvModule, NVL(pProcessName, 'FILE_MANAGER'), vProcName, pParameters, 'ERROR', 
              CASE 
                 WHEN LENGTH(vFullErrorMessage) <= 4000 THEN vFullErrorMessage
                 ELSE SUBSTR(vFullErrorMessage, 1, 3950) || '... [TRUNCATED]'
              END);

      COMMIT;

      -- Enhanced console output for immediate visibility (if enabled)
      IF gvConsoleLoggingEnabled = 'ON' THEN
         DBMS_OUTPUT.PUT_LINE('=======================================================');
         DBMS_OUTPUT.PUT_LINE('ERROR DETECTED AT: ' || vTimestamp);
         DBMS_OUTPUT.PUT_LINE('PROCEDURE: ' || NVL(vProcName, 'UNKNOWN'));
         DBMS_OUTPUT.PUT_LINE('MESSAGE: ' || pLogMessage);
         DBMS_OUTPUT.PUT_LINE('SQLCODE: ' || SQLCODE || ' | ENVIRONMENT: ' || gvEnv);
         -- Extract and show the most relevant file and line number
         IF INSTR(vAdjustedBacktrace, '-> ') > 0 THEN
            DBMS_OUTPUT.PUT_LINE('SOURCE FILE LOCATION: ' || REGEXP_SUBSTR(vAdjustedBacktrace, '-> [^)]+'));
         END IF;
         DBMS_OUTPUT.PUT_LINE('FULL DETAILS: Query A_PROCESS_LOG table for complete diagnostic info');
         DBMS_OUTPUT.PUT_LINE('QUERY (This Error): SELECT * FROM CT_MRDS.A_PROCESS_LOG WHERE GUID = ''' || guid || ''' ORDER BY LOG_TIMESTAMP DESC;');
         DBMS_OUTPUT.PUT_LINE('QUERY (Recent All): SELECT * FROM CT_MRDS.A_PROCESS_LOG WHERE LOG_TIMESTAMP >= SYSDATE - 1/1440 ORDER BY LOG_TIMESTAMP DESC;');
         DBMS_OUTPUT.PUT_LINE('=======================================================');
      END IF;

   END LOG_PROCESS_ERROR;

   ----------------------------------------------------------------------------------------------------

   FUNCTION ANALYZE_VALIDATION_ERRORS(
      pValidationLogTable VARCHAR2,
      pTemplateSchema VARCHAR2,
      pTemplateTable VARCHAR2,
      pCsvFileUri VARCHAR2
   ) RETURN VARCHAR2
   IS
      vAnalysisReport    CLOB := '';
      vCsvHeader         VARCHAR2(4000);
      vExpectedOrder     VARCHAR2(4000);
      vCsvOrder          VARCHAR2(4000);
      vErrorDetails      VARCHAR2(32000) := '';
      vSolutions         VARCHAR2(4000);
      vColumnMismatch    VARCHAR2(1000);
      vErrorCount        NUMBER := 0;
      vFirstDataError    VARCHAR2(1000);
      vErrorColumn       VARCHAR2(100);
      vErrorValue        VARCHAR2(500);
      vExpectedType      VARCHAR2(100);
      vTemplateColCount  NUMBER := 0;
      vCsvColCount       NUMBER := 0;
      vExcessColumns     VARCHAR2(2000);
      vCsvFirstLine      VARCHAR2(4000);

      -- Cursor for template table columns
      CURSOR c_template_columns IS
         SELECT COLUMN_NAME, DATA_TYPE, COLUMN_ID
         FROM ALL_TAB_COLUMNS 
         WHERE OWNER = UPPER(REGEXP_SUBSTR(pTemplateSchema || '.' || pTemplateTable, '^([^.]+)'))
           AND TABLE_NAME = UPPER(REGEXP_SUBSTR(pTemplateSchema || '.' || pTemplateTable, '\.(.+)$', 1, 1, NULL, 1))
         ORDER BY COLUMN_ID;

   BEGIN
      -- Build expected column order from template table and count columns
      FOR rec IN c_template_columns LOOP
         IF vExpectedOrder IS NOT NULL THEN
            vExpectedOrder := vExpectedOrder || ', ';
         END IF;
         vExpectedOrder := vExpectedOrder || rec.COLUMN_NAME;
         vTemplateColCount := vTemplateColCount + 1;
      END LOOP;

      -- Parse validation log table for errors and CSV structure
      BEGIN
         -- Try to extract error information from the validation log table
         EXECUTE IMMEDIATE 'SELECT COUNT(*) FROM ' || pValidationLogTable || 
                          ' WHERE record LIKE ''error processing column%'''
         INTO vErrorCount;

         -- Get first error details
         IF vErrorCount > 0 THEN
            EXECUTE IMMEDIATE 'SELECT record FROM ' || pValidationLogTable || 
                             ' WHERE record LIKE ''error processing column%'' AND ROWNUM = 1'
            INTO vFirstDataError;

            -- Parse error to extract column name and error type
            vErrorColumn := REGEXP_SUBSTR(vFirstDataError, 'error processing column ([A-Z_]+)', 1, 1, NULL, 1);

            -- Try to get the actual error value from ORA-01722 message
            BEGIN
               EXECUTE IMMEDIATE 'SELECT record FROM ' || pValidationLogTable || 
                                ' WHERE record LIKE ''ORA-01722%'' AND ROWNUM = 1'
               INTO vFirstDataError;
               vErrorValue := REGEXP_SUBSTR(vFirstDataError, 'string value containing ''([^'']+)''', 1, 1, NULL, 1);
            EXCEPTION
               WHEN NO_DATA_FOUND THEN
                  vErrorValue := 'unknown value';
               WHEN OTHERS THEN
                  vErrorValue := 'parsing error';
            END;
         END IF;

         -- Try to extract CSV structure from validation log field definitions
         BEGIN
            EXECUTE IMMEDIATE '
            SELECT LISTAGG(
               REGEXP_SUBSTR(record, ''^\s+([A-Z_]+)\s+'', 1, 1, NULL, 1), 
               '', ''
            ) WITHIN GROUP (ORDER BY ROWNUM)
            FROM ' || pValidationLogTable || '
            WHERE record LIKE ''    %CHAR%''
              AND record NOT LIKE ''%Fields in Data Source%''
              AND REGEXP_SUBSTR(record, ''^\s+([A-Z_]+)\s+'') IS NOT NULL'
            INTO vCsvOrder;

            -- Count CSV columns from parsed structure
            IF vCsvOrder IS NOT NULL THEN
               vCsvColCount := REGEXP_COUNT(vCsvOrder, ',') + 1;
            END IF;

         EXCEPTION
            WHEN OTHERS THEN
               vCsvOrder := 'Unable to determine CSV column order from validation log';
         END;

         -- Alternative method: Try to read first line of CSV directly for column count
         IF vCsvColCount = 0 THEN
            BEGIN
               -- This is a fallback - try to get CSV header from external source if possible
               -- Note: This would require DBMS_CLOUD.GET_OBJECT or similar approach
               -- For now, we'll rely on the validation log parsing
               NULL;
            EXCEPTION
               WHEN OTHERS THEN
                  NULL;
            END;
         END IF;

      EXCEPTION
         WHEN OTHERS THEN
            vErrorDetails := 'Error analyzing validation log: ' || SQLERRM;
      END;

      -- Detect column order mismatch and excess columns
      IF vCsvOrder IS NOT NULL AND vExpectedOrder IS NOT NULL THEN
         IF UPPER(REPLACE(vCsvOrder, ' ', '')) != UPPER(REPLACE(vExpectedOrder, ' ', '')) THEN
            vColumnMismatch := 'YES';
         ELSE
            vColumnMismatch := 'NO';
         END IF;
      END IF;

      -- Check for excess columns
      IF vCsvColCount > vTemplateColCount THEN
         -- Try to identify which columns are excess
         IF vCsvOrder IS NOT NULL THEN
            -- Parse CSV columns and compare with template
            DECLARE
               vCsvCols        SYS.ODCIVARCHAR2LIST;
               vTemplateCols   SYS.ODCIVARCHAR2LIST;
               vExcessFound    VARCHAR2(1) := 'N';
               i               NUMBER;
            BEGIN
               -- Split CSV columns  
               SELECT TRIM(REGEXP_SUBSTR(vCsvOrder, '[^,]+', 1, LEVEL))
               BULK COLLECT INTO vCsvCols
               FROM DUAL
               CONNECT BY REGEXP_SUBSTR(vCsvOrder, '[^,]+', 1, LEVEL) IS NOT NULL;

               -- Split template columns
               SELECT TRIM(REGEXP_SUBSTR(vExpectedOrder, '[^,]+', 1, LEVEL))
               BULK COLLECT INTO vTemplateCols  
               FROM DUAL
               CONNECT BY REGEXP_SUBSTR(vExpectedOrder, '[^,]+', 1, LEVEL) IS NOT NULL;

               -- Find excess columns (those in CSV but not in template)
               FOR i IN 1..vCsvCols.COUNT LOOP
                  DECLARE
                     vFoundInTemplate BOOLEAN := FALSE;
                     j NUMBER;
                  BEGIN
                     -- Check if CSV column exists in template
                     FOR j IN 1..vTemplateCols.COUNT LOOP
                        IF UPPER(TRIM(vCsvCols(i))) = UPPER(TRIM(vTemplateCols(j))) THEN
                           vFoundInTemplate := TRUE;
                           EXIT;
                        END IF;
                     END LOOP;

                     -- If not found in template, it's an excess column
                     IF NOT vFoundInTemplate THEN
                        IF vExcessFound = 'Y' THEN
                           vExcessColumns := vExcessColumns || ', ';
                        END IF;
                        vExcessColumns := vExcessColumns || vCsvCols(i);
                        vExcessFound := 'Y';
                     END IF;
                  END;
               END LOOP;
            EXCEPTION
               WHEN OTHERS THEN
                  vExcessColumns := 'Unable to determine specific excess columns';
            END;
         END IF;
      END IF;

      -- Build comprehensive analysis report
      vAnalysisReport := 'FILE VALIDATION FAILED - DETAILED ANALYSIS' || cgBL ||
                        '=================================================' || cgBL || cgBL;

      -- Column structure analysis  
      vAnalysisReport := vAnalysisReport || 
                        'COLUMN STRUCTURE ANALYSIS:' || cgBL ||
                        '---------------------------------------------------' || cgBL ||
                        'Template Expected Order:  ' || vExpectedOrder || cgBL ||
                        'Template Column Count:    ' || vTemplateColCount || cgBL ||
                        'CSV Detected Order:       ' || NVL(vCsvOrder, 'Unknown') || cgBL ||
                        'CSV Column Count:         ' || vCsvColCount || cgBL || cgBL;

      -- Report column count issues
      IF vCsvColCount > vTemplateColCount THEN
         vAnalysisReport := vAnalysisReport || 
                           'EXCESS COLUMNS DETECTED!' || cgBL ||
                           'CSV file has ' || (vCsvColCount - vTemplateColCount) || ' more columns than template allows.' || cgBL;
         IF vExcessColumns IS NOT NULL THEN
            vAnalysisReport := vAnalysisReport || 
                              'Excess columns found: ' || vExcessColumns || cgBL;
         END IF;
         vAnalysisReport := vAnalysisReport || cgBL;
      END IF;

      -- Report column order issues
      IF vColumnMismatch = 'YES' THEN
         vAnalysisReport := vAnalysisReport || 
                           'COLUMN ORDER MISMATCH DETECTED!' || cgBL ||
                           'CSV columns are in different order than template expects.' || cgBL || cgBL;
      END IF;

      -- Specific error analysis
      IF vErrorCount > 0 THEN
         vAnalysisReport := vAnalysisReport || 
                           'SPECIFIC ERRORS FOUND:' || cgBL ||
                           '---------------------------------------------------' || cgBL;

         IF vErrorColumn IS NOT NULL THEN
            -- Get expected data type for error column
            FOR rec IN c_template_columns LOOP
               IF rec.COLUMN_NAME = vErrorColumn THEN
                  vExpectedType := rec.DATA_TYPE;
                  EXIT;
               END IF;
            END LOOP;

            vAnalysisReport := vAnalysisReport || 
                              '1. Column ' || vErrorColumn || ': Expected ' || vExpectedType || 
                              ', received "' || NVL(vErrorValue, 'unknown value') || '" (TEXT)' || cgBL ||
                              '   → CSV position contains different data type than expected' || cgBL;
         END IF;

         vAnalysisReport := vAnalysisReport || 
                           'Total validation errors found: ' || vErrorCount || cgBL || cgBL;
      END IF;

      -- Solutions section
      vAnalysisReport := vAnalysisReport || 
                        'SUGGESTED SOLUTIONS:' || cgBL ||
                        '---------------------------------------------------' || cgBL;

      -- Solutions for excess columns
      IF vCsvColCount > vTemplateColCount THEN
         vAnalysisReport := vAnalysisReport || 
                           'FOR EXCESS COLUMNS:' || cgBL ||
                           '• Remove extra columns from CSV file' || cgBL ||
                           '• Keep only these columns in this order: ' || vExpectedOrder || cgBL;
         IF vExcessColumns IS NOT NULL THEN
            vAnalysisReport := vAnalysisReport || 
                              '• Specifically remove: ' || vExcessColumns || cgBL;
         END IF;
         vAnalysisReport := vAnalysisReport || cgBL;
      END IF;

      -- Solutions for column order
      IF vColumnMismatch = 'YES' THEN
         vAnalysisReport := vAnalysisReport || 
                           'FOR COLUMN ORDER:' || cgBL ||
                           '• Reorder CSV columns to match template: ' || vExpectedOrder || cgBL ||
                           '• Or update template table column order to match CSV file' || cgBL || cgBL;
      END IF;

      -- General solutions
      vAnalysisReport := vAnalysisReport || 
                        'GENERAL RECOMMENDATIONS:' || cgBL ||
                        '• Ensure CSV has exactly ' || vTemplateColCount || ' columns' || cgBL ||
                        '• Verify column names match template table exactly' || cgBL ||
                        '• Check data types in each column match expectations' || cgBL || cgBL;

      -- Validation log reference
      vAnalysisReport := vAnalysisReport || 
                        'TECHNICAL DETAILS:' || cgBL ||
                        '---------------------------------------------------' || cgBL ||
                        'Validation Log Table: ' || pValidationLogTable || cgBL ||
                        'Template Table: ' || pTemplateSchema || '.' || pTemplateTable || cgBL ||
                        'CSV File: ' || pCsvFileUri || cgBL ||
                        'Query validation details: SELECT * FROM ' || pValidationLogTable || ';' || cgBL;

      RETURN vAnalysisReport;

   EXCEPTION
      WHEN OTHERS THEN
         RETURN 'Error generating validation analysis: ' || SQLERRM || cgBL ||
                'Validation Log Table: ' || pValidationLogTable || cgBL ||
                'Check table manually: SELECT * FROM ' || pValidationLogTable || ';';
   END ANALYZE_VALIDATION_ERRORS;

   ----------------------------------------------------------------------------------------------------
   -- PACKAGE VERSION MANAGEMENT FUNCTIONS IMPLEMENTATION
   ----------------------------------------------------------------------------------------------------

   FUNCTION GET_VERSION
   RETURN VARCHAR2
   IS
   BEGIN
      RETURN PACKAGE_VERSION;
   END GET_VERSION;

   ----------------------------------------------------------------------------------------------------

   FUNCTION GET_BUILD_INFO
   RETURN VARCHAR2
   IS
   BEGIN
      RETURN GET_PACKAGE_VERSION_INFO(
         pPackageName => 'ENV_MANAGER',
         pVersion => PACKAGE_VERSION,
         pBuildDate => PACKAGE_BUILD_DATE,
         pAuthor => PACKAGE_AUTHOR
      );
   END GET_BUILD_INFO;

   ----------------------------------------------------------------------------------------------------

   FUNCTION GET_VERSION_HISTORY
   RETURN VARCHAR2
   IS
   BEGIN
      RETURN FORMAT_VERSION_HISTORY(
         pPackageName => 'ENV_MANAGER',
         pVersionHistory => VERSION_HISTORY
      );
   END GET_VERSION_HISTORY;

   ----------------------------------------------------------------------------------------------------

   FUNCTION GET_PACKAGE_VERSION_INFO(
      pPackageName VARCHAR2,
      pVersion VARCHAR2,
      pBuildDate VARCHAR2,
      pAuthor VARCHAR2
   ) RETURN VARCHAR2
   IS
   BEGIN
      RETURN 'Package: ' || pPackageName || cgBL ||
             'Version: ' || pVersion || cgBL ||
             'Build Date: ' || pBuildDate || cgBL ||
             'Author: ' || pAuthor;
   END GET_PACKAGE_VERSION_INFO;

   ----------------------------------------------------------------------------------------------------

   FUNCTION FORMAT_VERSION_HISTORY(
      pPackageName VARCHAR2,
      pVersionHistory VARCHAR2
   ) RETURN VARCHAR2
   IS
   BEGIN
      RETURN pPackageName || ' Version History:' || cgBL || pVersionHistory;
   END FORMAT_VERSION_HISTORY;

   ----------------------------------------------------------------------------------------------------
   -- PACKAGE HASH + CHANGE DETECTION FUNCTIONS IMPLEMENTATION
   ----------------------------------------------------------------------------------------------------

   FUNCTION CALCULATE_PACKAGE_HASH(
      pPackageOwner VARCHAR2,
      pPackageName VARCHAR2,
      pPackageType VARCHAR2
   ) RETURN VARCHAR2
   IS
      vSourceCode    CLOB;
      vHash          VARCHAR2(64);
      vRawHash       RAW(32);
   BEGIN
      -- Build complete source code from ALL_SOURCE using XMLAGG (no 4000 char limit)
      -- CRITICAL: Cannot use LISTAGG due to VARCHAR2 limit
      SELECT XMLAGG(XMLELEMENT(E, TEXT) ORDER BY LINE).GETCLOBVAL()
      INTO vSourceCode
      FROM ALL_SOURCE
      WHERE OWNER = UPPER(pPackageOwner)
        AND NAME = UPPER(pPackageName)
        AND TYPE = UPPER(pPackageType);

      -- If empty, return NULL
      IF vSourceCode IS NULL OR DBMS_LOB.GETLENGTH(vSourceCode) = 0 THEN
         RETURN NULL;
      END IF;

      -- Calculate SHA256 hash directly from CLOB
      -- DBMS_CRYPTO.HASH has overload for CLOB in Oracle 19c+
      vRawHash := DBMS_CRYPTO.HASH(
         src => vSourceCode,
         typ => DBMS_CRYPTO.HASH_SH256
      );

      -- Convert to hex string
      vHash := LOWER(RAWTOHEX(vRawHash));

      RETURN vHash;

   EXCEPTION
      WHEN NO_DATA_FOUND THEN
         RETURN NULL;
      WHEN OTHERS THEN
         LOG_PROCESS_ERROR('Error calculating package hash: ' || SQLERRM,
                          'pPackageOwner=' || pPackageOwner || ', pPackageName=' || pPackageName);
         RETURN NULL;
   END CALCULATE_PACKAGE_HASH;

   ----------------------------------------------------------------------------------------------------

   PROCEDURE TRACK_PACKAGE_VERSION(
      pPackageOwner VARCHAR2,
      pPackageName VARCHAR2,
      pPackageVersion VARCHAR2,
      pPackageBuildDate VARCHAR2,
      pPackageAuthor VARCHAR2
   )
   IS
      vHashSpec              VARCHAR2(64);
      vHashBody              VARCHAR2(64);
      vLastHashSpec          VARCHAR2(64);
      vLastHashBody          VARCHAR2(64);
      vLastVersion           VARCHAR2(10);
      vLineCountSpec         NUMBER;
      vLineCountBody         NUMBER;
      vChangeDetected        CHAR(1) := 'N';
      vChangeMessage         VARCHAR2(4000);
      vParameters            VARCHAR2(4000);
   BEGIN
      vParameters := FORMAT_PARAMETERS(SYS.ODCIVARCHAR2LIST(
         'pPackageOwner => ''' || pPackageOwner || '''',
         'pPackageName => ''' || pPackageName || '''',
         'pPackageVersion => ''' || pPackageVersion || ''''
      ));

      LOG_PROCESS_EVENT('Start TRACK_PACKAGE_VERSION', 'INFO', vParameters);

      -- Calculate current hashes
      vHashSpec := CALCULATE_PACKAGE_HASH(pPackageOwner, pPackageName, 'PACKAGE');
      vHashBody := CALCULATE_PACKAGE_HASH(pPackageOwner, pPackageName, 'PACKAGE BODY');

      -- Get line counts
      BEGIN
         SELECT COUNT(*)
         INTO vLineCountSpec
         FROM ALL_SOURCE
         WHERE OWNER = UPPER(pPackageOwner)
           AND NAME = UPPER(pPackageName)
           AND TYPE = 'PACKAGE';
      EXCEPTION
         WHEN NO_DATA_FOUND THEN
            vLineCountSpec := 0;
      END;

      BEGIN
         SELECT COUNT(*)
         INTO vLineCountBody
         FROM ALL_SOURCE
         WHERE OWNER = UPPER(pPackageOwner)
           AND NAME = UPPER(pPackageName)
           AND TYPE = 'PACKAGE BODY';
      EXCEPTION
         WHEN NO_DATA_FOUND THEN
            vLineCountBody := 0;
      END;

      -- Get last tracked version and hashes
      BEGIN
         SELECT PACKAGE_VERSION, SOURCE_CODE_HASH_SPEC, SOURCE_CODE_HASH_BODY
         INTO vLastVersion, vLastHashSpec, vLastHashBody
         FROM CT_MRDS.A_PACKAGE_VERSION_TRACKING
         WHERE PACKAGE_OWNER = UPPER(pPackageOwner)
           AND PACKAGE_NAME = UPPER(pPackageName)
         ORDER BY TRACKING_DATE DESC
         FETCH FIRST 1 ROW ONLY;

         -- Check if hash changed but version didn't
         IF (vHashSpec != vLastHashSpec OR NVL(vHashBody,'X') != NVL(vLastHashBody,'X'))
            AND pPackageVersion = vLastVersion THEN

            vChangeDetected := 'Y';
            vChangeMessage := 'WARNING: Source code changed without version update!' || cgBL ||
                             'Last Version: ' || vLastVersion || cgBL ||
                             'Current Version: ' || pPackageVersion || cgBL;

            IF vHashSpec != vLastHashSpec THEN
               vChangeMessage := vChangeMessage || 
                                'SPEC Changed - Hash: ' || SUBSTR(vHashSpec, 1, 16) || '... (was: ' || 
                                SUBSTR(vLastHashSpec, 1, 16) || '...)' || cgBL;
            END IF;

            IF NVL(vHashBody,'X') != NVL(vLastHashBody,'X') THEN
               vChangeMessage := vChangeMessage || 
                                'BODY Changed - Hash: ' || SUBSTR(vHashBody, 1, 16) || '... (was: ' || 
                                SUBSTR(NVL(vLastHashBody,'NULL'), 1, 16) || '...)' || cgBL;
            END IF;

            vChangeMessage := vChangeMessage || 
                             'RECOMMENDATION: Update PACKAGE_VERSION constant and PACKAGE_BUILD_DATE';

            LOG_PROCESS_EVENT(vChangeMessage, 'WARNING', vParameters);
         END IF;

      EXCEPTION
         WHEN NO_DATA_FOUND THEN
            -- First time tracking this package
            vChangeDetected := 'N';
            vChangeMessage := 'First tracking record for this package';
            LOG_PROCESS_EVENT(vChangeMessage, 'INFO', vParameters);
      END;

      -- Insert tracking record
      INSERT INTO CT_MRDS.A_PACKAGE_VERSION_TRACKING (
         PACKAGE_OWNER,
         PACKAGE_NAME,
         PACKAGE_TYPE,
         PACKAGE_VERSION,
         PACKAGE_BUILD_DATE,
         PACKAGE_AUTHOR,
         SOURCE_CODE_HASH_SPEC,
         SOURCE_CODE_HASH_BODY,
         LINE_COUNT_SPEC,
         LINE_COUNT_BODY,
         DETECTED_CHANGE_WITHOUT_VERSION,
         CHANGE_DETECTION_MESSAGE
      ) VALUES (
         UPPER(pPackageOwner),
         UPPER(pPackageName),
         'BOTH',
         pPackageVersion,
         pPackageBuildDate,
         pPackageAuthor,
         vHashSpec,
         vHashBody,
         vLineCountSpec,
         vLineCountBody,
         vChangeDetected,
         vChangeMessage
      );

      COMMIT;

      LOG_PROCESS_EVENT('End TRACK_PACKAGE_VERSION - Record inserted', 'INFO', vParameters);

   EXCEPTION
      WHEN OTHERS THEN
         LOG_PROCESS_ERROR('Error in TRACK_PACKAGE_VERSION: ' || SQLERRM, vParameters);
         RAISE;
   END TRACK_PACKAGE_VERSION;

   ----------------------------------------------------------------------------------------------------

   FUNCTION CHECK_PACKAGE_CHANGES(
      pPackageOwner VARCHAR2,
      pPackageName VARCHAR2
   ) RETURN VARCHAR2
   IS
      vCurrentHashSpec       VARCHAR2(64);
      vCurrentHashBody       VARCHAR2(64);
      vLastHashSpec          VARCHAR2(64);
      vLastHashBody          VARCHAR2(64);
      vLastVersion           VARCHAR2(10);
      vLastTrackingDate      TIMESTAMP;
      vChangeReport          VARCHAR2(4000);
      vSpecChanged           BOOLEAN := FALSE;
      vBodyChanged           BOOLEAN := FALSE;
   BEGIN
      -- Get current hashes
      vCurrentHashSpec := CALCULATE_PACKAGE_HASH(pPackageOwner, pPackageName, 'PACKAGE');
      vCurrentHashBody := CALCULATE_PACKAGE_HASH(pPackageOwner, pPackageName, 'PACKAGE BODY');

      -- Get last tracked hashes
      BEGIN
         SELECT PACKAGE_VERSION, SOURCE_CODE_HASH_SPEC, SOURCE_CODE_HASH_BODY, TRACKING_DATE
         INTO vLastVersion, vLastHashSpec, vLastHashBody, vLastTrackingDate
         FROM CT_MRDS.A_PACKAGE_VERSION_TRACKING
         WHERE PACKAGE_OWNER = UPPER(pPackageOwner)
           AND PACKAGE_NAME = UPPER(pPackageName)
         ORDER BY TRACKING_DATE DESC
         FETCH FIRST 1 ROW ONLY;
      EXCEPTION
         WHEN NO_DATA_FOUND THEN
            RETURN 'Package ' || pPackageOwner || '.' || pPackageName || ' has never been tracked.' || cgBL ||
                   'Run TRACK_PACKAGE_VERSION to establish baseline.';
      END;

      -- Check for changes
      IF vCurrentHashSpec != vLastHashSpec THEN
         vSpecChanged := TRUE;
      END IF;

      IF NVL(vCurrentHashBody, 'X') != NVL(vLastHashBody, 'X') THEN
         vBodyChanged := TRUE;
      END IF;

      -- Build report
      IF vSpecChanged OR vBodyChanged THEN
         vChangeReport := 'WARNING: Package ' || pPackageOwner || '.' || pPackageName || ' has changed!' || cgBL ||
                         '========================================' || cgBL ||
                         'Last Tracked Version: ' || vLastVersion || cgBL ||
                         'Last Tracked Date: ' || TO_CHAR(vLastTrackingDate, 'YYYY-MM-DD HH24:MI:SS') || cgBL ||
                         cgBL;

         IF vSpecChanged THEN
            vChangeReport := vChangeReport || 
                            'SPECIFICATION Changed:' || cgBL ||
                            '  Current Hash: ' || SUBSTR(vCurrentHashSpec, 1, 16) || '...' || cgBL ||
                            '  Last Hash:    ' || SUBSTR(vLastHashSpec, 1, 16) || '...' || cgBL ||
                            cgBL;
         END IF;

         IF vBodyChanged THEN
            vChangeReport := vChangeReport || 
                            'BODY Changed:' || cgBL ||
                            '  Current Hash: ' || SUBSTR(NVL(vCurrentHashBody, 'NULL'), 1, 16) || '...' || cgBL ||
                            '  Last Hash:    ' || SUBSTR(NVL(vLastHashBody, 'NULL'), 1, 16) || '...' || cgBL ||
                            cgBL;
         END IF;

         vChangeReport := vChangeReport || 
                         'RECOMMENDATION:' || cgBL ||
                         '1. Update PACKAGE_VERSION constant' || cgBL ||
                         '2. Update PACKAGE_BUILD_DATE constant' || cgBL ||
                         '3. Add entry to VERSION_HISTORY' || cgBL ||
                         '4. Call TRACK_PACKAGE_VERSION to update tracking';
      ELSE
         vChangeReport := 'OK: Package ' || pPackageOwner || '.' || pPackageName || ' has not changed.' || cgBL ||
                         'Last Tracked: ' || TO_CHAR(vLastTrackingDate, 'YYYY-MM-DD HH24:MI:SS') || cgBL ||
                         'Version: ' || vLastVersion;
      END IF;

      RETURN vChangeReport;

   EXCEPTION
      WHEN OTHERS THEN
         RETURN 'Error checking package changes: ' || SQLERRM;
   END CHECK_PACKAGE_CHANGES;

   ----------------------------------------------------------------------------------------------------

   FUNCTION GET_PACKAGE_HASH_INFO(
      pPackageOwner VARCHAR2,
      pPackageName VARCHAR2
   ) RETURN VARCHAR2
   IS
      vCurrentHashSpec       VARCHAR2(64);
      vCurrentHashBody       VARCHAR2(64);
      vLastHashSpec          VARCHAR2(64);
      vLastHashBody          VARCHAR2(64);
      vLastVersion           VARCHAR2(10);
      vLastTrackingDate      TIMESTAMP;
      vLastChangeDetected    CHAR(1);
      vInfo                  VARCHAR2(4000);
   BEGIN
      -- Get current hashes
      vCurrentHashSpec := CALCULATE_PACKAGE_HASH(pPackageOwner, pPackageName, 'PACKAGE');
      vCurrentHashBody := CALCULATE_PACKAGE_HASH(pPackageOwner, pPackageName, 'PACKAGE BODY');

      -- Get last tracking info
      BEGIN
         SELECT PACKAGE_VERSION, 
                SOURCE_CODE_HASH_SPEC, 
                SOURCE_CODE_HASH_BODY, 
                TRACKING_DATE,
                DETECTED_CHANGE_WITHOUT_VERSION
         INTO vLastVersion, vLastHashSpec, vLastHashBody, vLastTrackingDate, vLastChangeDetected
         FROM CT_MRDS.A_PACKAGE_VERSION_TRACKING
         WHERE PACKAGE_OWNER = UPPER(pPackageOwner)
           AND PACKAGE_NAME = UPPER(pPackageName)
         ORDER BY TRACKING_DATE DESC
         FETCH FIRST 1 ROW ONLY;
      EXCEPTION
         WHEN NO_DATA_FOUND THEN
            RETURN 'Package: ' || pPackageOwner || '.' || pPackageName || cgBL ||
                   'Status: Never tracked' || cgBL ||
                   'Current Hash (SPEC): ' || SUBSTR(vCurrentHashSpec, 1, 16) || '...' || cgBL ||
                   'Current Hash (BODY): ' || SUBSTR(NVL(vCurrentHashBody, 'NULL'), 1, 16) || '...';
      END;

      -- Build info report
      vInfo := 'Package: ' || pPackageOwner || '.' || pPackageName || cgBL ||
               'Current Version: ' || vLastVersion || cgBL ||
               'Last Tracked: ' || TO_CHAR(vLastTrackingDate, 'YYYY-MM-DD HH24:MI:SS') || cgBL ||
               cgBL ||
               'Current Hash (SPEC): ' || SUBSTR(vCurrentHashSpec, 1, 32) || '...' || cgBL ||
               'Last Hash (SPEC):    ' || SUBSTR(vLastHashSpec, 1, 32) || '...' || cgBL;

      IF vCurrentHashBody IS NOT NULL OR vLastHashBody IS NOT NULL THEN
         vInfo := vInfo || 
                  'Current Hash (BODY): ' || SUBSTR(NVL(vCurrentHashBody, 'NULL'), 1, 32) || '...' || cgBL ||
                  'Last Hash (BODY):    ' || SUBSTR(NVL(vLastHashBody, 'NULL'), 1, 32) || '...' || cgBL;
      END IF;

      vInfo := vInfo || cgBL;

      -- Status
      IF vCurrentHashSpec = vLastHashSpec AND NVL(vCurrentHashBody, 'X') = NVL(vLastHashBody, 'X') THEN
         vInfo := vInfo || 'Status: OK - No changes detected';
      ELSE
         vInfo := vInfo || 'Status: CHANGED - Source code modified since last tracking';
      END IF;

      IF vLastChangeDetected = 'Y' THEN
         vInfo := vInfo || cgBL || 'Last Tracking Warning: Change detected without version update';
      END IF;

      RETURN vInfo;

   EXCEPTION
      WHEN OTHERS THEN
         RETURN 'Error getting package hash info: ' || SQLERRM;
   END GET_PACKAGE_HASH_INFO;

   ----------------------------------------------------------------------------------------------------

BEGIN
   INIT_ERRORS;
   guid        := sys_guid();
   gvUsername  := SYS_CONTEXT('USERENV', 'SESSION_USER');
   gvOsuser    := SYS_CONTEXT('USERENV', 'OS_USER');
   gvMachine   := SYS_CONTEXT('USERENV', 'HOST');
   gvModule    := SYS_CONTEXT('USERENV', 'MODULE');

   -- Get info about EnvironmentID. Without it package cannot proceed further.
   -- Information about environment is needed to get proper configuration values
   -- It can be set up in two different ways :
   -- 1. Set it on session level:       execute DBMS_SESSION.SET_IDENTIFIER (client_id => 'dev');
   -- 2. Set it on configuration level: Insert into CT_MRDS.A_FILE_MANAGER_CONFIG (ENVIRONMENT_ID,CONFIG_VARIABLE,CONFIG_VARIABLE_VALUE) values ('default','environment_id','dev');
   -- Session level setup (1.) takes precedence over configuration level one (2.)

   gvEnv := nvl(SYS_CONTEXT ('USERENV', 'CLIENT_IDENTIFIER'), GET_DEFAULT_ENV());
   if gvEnv is null then
      dbms_output.put_line(MSG_ENVIRONMENT_NOT_SET);
      LOG_PROCESS_EVENT(MSG_ENVIRONMENT_NOT_SET, 'ERROR');
      RAISE_APPLICATION_ERROR(CODE_ENVIRONMENT_NOT_SET, MSG_ENVIRONMENT_NOT_SET);
   else
      dbms_output.put_line('EnvironmentID set to: '||gvEnv);
   end if;

   INIT_VARIABLES(pEnv => gvEnv);
END ENV_MANAGER;

/

/
